Source code style guide

Cargo.toml

Follow the official formatting conventions for the Cargo.toml file. This means:

  • Put the [package] section at the top of the file.

  • Put the name and version keys in that order at the top of that section, followed by the remaining keys other than description in order (sort keys with version-sort; very similar to lexical sorting)), followed by the description at the end of that section.

  • For other sections, sort keys with version-sort.

[package]
name = "crate-name"
version = "0.0.1"
# ... otherwise alphabetically sorted here
# ... and then the description as the last key
description = "this crate does nothing"

[dependencies]
# dependencies sorted alphabetically
a_dependency = "1.2.3"
another_dependency = "0.1.0"
yet_another_dependency = "0.2.0"
[package]
description = "this crate does nothing"
name = "crate-name"
version = "0.0.1"

[dependencies]
another_dependency = "0.1.0"
yet_another_dependency = "0.2.0"
a_dependency = "1.2.3"
This formatting might be supported by rustfmt in the future, see the PR here.

Identifier names

Long versus abbreviated

We use unabbreviated identifier names to avoid ambiguity. Short (even single letter) variable names are allowed in lambdas (closures), in one-liners, and when the context allows it.

The shorter the scope the shorter the variable names, and the longer the function […​] names. And vice versa.
— Uncle Bob Martin
Source: Twitter

The usage of well-known acronyms like CPU, TLS or OIDC are allowed.

const ONE_HOUR_IN_SECONDS: usize = 3600;

let parameter = Some("foo");
let buffer = &[];

fn function(elements: Vec<String>) {}

for i in 0..5 {}
const ONE_H_IN_SECS: usize = 3600;

let param = Some("foo");
let buf = &[];

fn func(elems: Vec<String>) {}

Closures and one-liners

It should be noted that the second example is meant to illustrate the use of single letter variable names in closures. It does not reflect production-level Rust code. The snippet would be simplified in the real world.

let length = parameter.map(|p| p.len());

let sum: usize = vec![Some(2), None, Some(4), Some(3), None]
    .iter()
    .filter(|o| o.is_some())
    .map(|n| n.unwrap())
    .map(|n| n * 2)
    .sum()

Well-known acronyms

const K8S_LABEL_KEY: &str = "app.kubernetes.io";

let oidc_provider = OidcProvider {};
let tls_settings = TlsSettings {};

Optional function parameters and variables

Optional function parameters and variables containing Option must not use any prefixes or suffixes to indicate the value is of type Option. This rule does not apply to function names like Client::get_opt().

let tls_settings: Option<TlsSettings> = None;
let tls_settings_or_none: Option<TlsSettings> = None;
let maybe_tls_settings: Option<TlsSettings> = None;
let opt_tls_settings: Option<TlsSettings> = None;

Structs and enums

Naming convention

Structs can use singular and plural names. Enums must use singular names, because only one variant is valid, e.g. Error::NotFound and not Errors::NotFound.

enum Error {
  NotFound,
  Timeout,
}

enum Color {
  Red,
  Green,
  Blue,
}
enum Errors {
  NotFound,
  Timeout,
}

enum Colors {
  Red,
  Green,
  Blue,
}

Formatting of struct fields and enum variants

Add newlines to struct fields and enum variants when they include additional information like documentation comments or attributes, because the variants can become difficult to read. This is especially the case when fields include doc comments, attributes like #[snafu()], and in case of enum variants, various embedded types.

Enum variants and struct fields don’t need to be separated when no additional information is attached to any of the variants or fields.

enum Color {
    Red,
    Green,
    Blue,
}

struct Foo {
    /// My doc comment for bar
    bar: usize,

    /// My doc comment for baz
    baz: usize,
}

enum Error {
    /// Indicates that we failed to foo.
    #[snafu(display("failed to foo"))]
    Foo,

    /// Indicates that we failed to bar.
    #[snafu(display("failed to bar"))]
    Bar,
    Baz,
}
enum Color {
    Red,

    Green,

    Blue,
}

struct Foo {
    /// My doc comment for bar
    bar: usize,
    /// My doc comment for baz
    baz: usize,
}

enum Error {
    /// Indicates that we failed to foo.
    #[snafu(display("failed to foo"))]
    Foo,
    /// Indicates that we failed to bar.
    #[snafu(display("failed to bar"))]
    Bar,
    Baz,
}

Any single uncommented variants or fields in an otherwise-commented enum or struct is considered to be a smell. If any of the items are commented, all items should be. It should however also be noted that there is no requirement to comment fields or variants. Comments should only be added if they provide additional information not available from context.

Error handling

Choice of error crate and usage

Use snafu for all error handling in library and application code to provide as much context to the user as possible. Further, snafu allows us to use the same source error in multiple error variants. This feature can be used for cases where more fine-grained error variants are required. This behaviour is not possible when using thiserror, as it uses the From trait to convert the source error into an error variant.

Additionally, the usage of the #[snafu(context(false))] atrribute on error variants is restricted. This ensures that fallible functions need to call .context() to pass the error along.

The usage of thiserror is considered invalid.

#[derive(Snafu)]
enum Error {
    #[snafu(display("failed to read config file of user {user_name}"))]
    FileRead {
        source: std::io::Error,
        user_name: String,
    }
}

fn config_file(user: User) -> Result<(), Error> {
    std::fs::read_to_string(user.file_path).context(FileReadSnafu {
        user_name: user.name,
    });
}
#[derive(thiserror::Error)]
enum Error {
    #[error("failed to read config file")]
    FileRead(#[from] std::io::Error)
}

fn config_file(user: User) -> Result<(), Error> {
    std::fs::read_to_string(user.file_path)?;
}
#[derive(Snafu)]
enum Error {
    #[snafu(context(false))]
    FileRead { source: std::io::Error }
}

fn config_file(user: User) -> Result<(), Error> {
    std::fs::read_to_string(user.file_path)?;
}

Error variant names

All error variants must not include any unnesecarry prefixes or suffixes. Examples of such prefixes include (but are not limited to) FailedTo and UnableTo. Furthermore, examples for suffixes are Error or Snafu. Error variant names must however include verbs or identifiers as a prefix.

#[derive(Snafu)]
enum Error {
    ParseConfig,
    HttpRequest,
    ReadConfig,
}
#[derive(Snafu)]
enum Error {
    FailedToParseConfig,
    HttpRequestError,
    ConfigRead,
}

Error messages

All our error messages must start with a lowercase letter and must not end with a dot. It is recommended to start the error messages with "failed to…​" or "unable to …​".

Parameterised values need a clear distinction between them and the rest of the error message. These values must be wrapped by double quotes " and must use the Debug implementation for output. For types which don’t add double quotes around its value, the developer needs to add them manually. Most types other than String don’t wrap their values in double quotes.

#[derive(Snafu)]
enum Error {
    #[snafu(display("failed to foo"))]
    Foo,

    #[snafu(display("unable to bar"))]
    Bar,

    #[snafu(display("failed to baz {name:?}, received code \"{code:?}\""))]
    Baz {
        name: String,
        code: usize,
    },
}
#[derive(Snafu)]
enum Error {
    #[snafu(display("Foo happened."))]
    Foo,

    #[snafu(display("Bar encountered"))]
    Bar,

    #[snafu(display("failed to baz {name}, received code {code:?}"))]
    Baz {
        name: String,
        code: usize,
    },

    #[snafu(display("arghh foo bar."))]
    FooBar,
}

Examples for "failed to …​" error messages

  1. failed to parse config file to indicate the parsing of the config file failed, usually because the file doesn’t conform to the configuration language.

  2. failed to construct http client to indicate that the construction of a HTTP client to retrieve remote content failed.

Exampled for "unable to …​" error messages

  1. unable to read config file from …​ to indicate that the file could not be loaded (for example because the file doesn’t exist).

  2. unable to parse value …​ to indicate that parsing a user provided value failed (for example because it didn’t conform to the expected syntax).

String formatting

Named versus unnamed format string identifiers

For simple string formatting (up to two substitutions), unnamed (and thus also uncaptured) identifiers are allowed.

For more complex formatting (more than two substitutions), named identifiers are required to avoid ambiguity, and to decouple argument order from the text (which can lead to incorrect text when the wording is changed and {} are reordered while the arguments aren’t). This rule needs to strike a balance between explicitness and concise format!() invocations. Long format!() expressions can lead to rustfmt breakage. It might be better to split up long formatting strings into multiple smaller ones.

Mix-and-matching of named versus unnamed identifiers must be avoided. See the next section about captured versus uncaptured identifiers.

format!(
    "My {quantifier} {adjective} string with {count} substitutions is {description}!",
    quantifier = "super",
    adjective = "long",
    count = 4,
    description = "crazy",
);
format!(
    "My {} {} string with {} substitutions is {}!",
    "super",
    "long",
    4,
    "crazy",
);

format!(
    "My {quantifier} {} string with {count} substitutions is {}!",
    quantifier = "super",
    "long",
    count = 4,
    "crazy",
);

Captured versus uncaptured format string identifiers

There are no restrictions on named format string identifiers. All options below are considered valid.

let greetee = "world";

format!("Hello, {greetee}!");
format!("Hello, {greetee}!", greetee = "universe");
format!("Hello {name}, hello again {name}", name = greetee);

Specifying resources measured in bytes and CPU fractions

Follow the Kubernetes convention described here.

Resources measured in bytes

let memory: MemoryQuantity = "100Mi".parse();
let memory: MemoryQuantity = "1Gi".parse();
let memory: MemoryQuantity = "1536Mi".parse();
let memory: MemoryQuantity = "10Gi".parse();
// Biggest matching unit
let memory: MemoryQuantity = "100Mi".parse();
let memory: MemoryQuantity = "1Gi".parse();
let memory: MemoryQuantity = "1.5Gi".parse();
let memory: MemoryQuantity = "10Gi".parse();

// Always Mi
let memory: MemoryQuantity = "100Mi".parse();
let memory: MemoryQuantity = "1024Mi".parse();
let memory: MemoryQuantity = "1536Mi".parse();
let memory: MemoryQuantity = "10240Mi".parse();

// No unit at all
let memory: MemoryQuantity = "12345678".parse();

Resources measured in CPU fractions

let memory: CpuQuantity = "100m".parse();
let memory: CpuQuantity = "500m".parse();
let memory: CpuQuantity = "1".parse();
let memory: CpuQuantity = "2".parse();
// Always m
let memory: CpuQuantity = "100m".parse();
let memory: CpuQuantity = "500m".parse();
let memory: CpuQuantity = "1000m".parse();
let memory: CpuQuantity = "2000m".parse();

// Floating points
let memory: CpuQuantity = "0.1".parse();
let memory: CpuQuantity = "0.5".parse();
let memory: CpuQuantity = "1".parse();
let memory: CpuQuantity = "2".parse();