Coding Conventions

Basis

Unless otherwise specified below, our Rust coding convention is identical to the following resources:

Note that some of the guidance in the API Guidelines document is specifically applicable to public APIs of libraries. This doesn't necessarily apply to all of the code we write, although if we deviate from the API guidelines it should be deliberately and with a good reason.

Guiding Principle

The purpose of having a style guide and conventions is to produce consistent, readable, clear, and maintainable code. There will be situations in which a strict adherence to style conventions will be counter-productive to these objectives. Fortunately, Elastio team members are highly intelligent, educated, rational people who are capable of making subjective judgement calls based on the best information available to them.

Simply put, use your best judgement. If a rule seems to make code worse, then don't follow it (but be prepared to defend your decision in code review, and to reverse it if you're wrong).

Basics

All code should be formatted with the rustfmt utility with the default settings. This can be made automatic in most editors. Code that isn’t formatted this way will fail the CI build.

Code should compile without any warnings, and CI builds will treat warnings as errors.

All code should pass clippy static analysis with the default lints enabled. Failure to pass any of these will fail the CI build. If a particular clippy link is not applicable it should be disabled with the finest granularity possible (eg, at the variable or function or statement or at most module level; never at the crate level).

Using use

use is used to import modules or specific types into the current scope.

For large code files this can get complicated, so the goal of our conventions is to ensure we import other types in a way that maximizes readability and clarity.

Grouping

There should be one single block of use statements at the beginning of the module to which they apply. There should be no white space between the use statements. They should be sorted alphabetically, though as long as you adhere to the preceding rules rustfmt will handle sorting them automatically.

Importing a module

In the majority of cases, it's preferable to import a module, not the item(s) in that module.

For example, if you want to call foo in the bar module in the widget crate, you should do this:

use widget::bar;

// ...

bar::foo();

This makes it very clear that foo is not an item in the current module but is imported from elsewhere.

Importing a type

Exceptions to the above rule do exist. Remember that the goal is clarity. If you're implementing Debug, it's okay to use std::fmt::Debug; we all know what Debug is and won't need to scratch our heads to figure out what it means.

Another exception is types that are used frequently throughout a particular crate. For example in the RocksDB wrapper crate, there's a trait BinaryStr that is used all over in various modules. There's not much value in qualifying it as ops::BinaryStr, as its origin and purpose are readily apparent to anyone working in the code.

A third and very common exception is when calling methods on a trait. Rust requires that the trait be in scope in order to call methods on it, and in these cases there is no reasonable alternative but to explicitly use the trait itself.

Lastly, it's often the case that the name of a type is sufficient to make it clear where it's from. For example, Serialize and Deserialize are obviously from the serde crate. MyCrateError is obviously an error type from my_crate, and does not need to be qualified as my_crate::MyCrateError for clarity. This is a matter of good judgement and may be questioned in code review.

Wildcard imports

Wildcard imports are to be considered forbidden except under specific circumstances:

  1. When a crate exposes a prelude module for convenience. An example of this is the Rayon crate, which has rayon::prelude specifically to provide a single place to import all of the helper traits and extensions to existing traits. Especially if this module is called prelude it's apparent by convention what it's for, and thus a wildcard import of that module can be used whenever it makes sense.

  2. When bringing into scope types whose actual location are obvious from context or not relevant to understanding the code. This is obviously a very subjective standard, and you should be prepared to defend this assessment in a code review if you rely on it for justification of a wildcard import.

  3. In a lib.rs file when re-exporting items from private modules. It's almost always good practice to organize code into logical modules, even if those modules themselves are not public. In that case one must re-export the relevant types, and there's usually not any descriptive or clarifying value in explicitly listing each of the items one is re-exporting.

In general wildcard imports obscure the origin of the types being used and make it harder to understand the code at a glance. Especially since tools like rust-analyze and IntelliJ can automatically generate import statements for specific types as they are used, there's no excuse for lazily pulling in everything in a module.

Error Handling

How to represent, handle, and raise errors is a big topic. See the Errors chapter.