Coding Guides
Introduction
We present coding guides for the reliable message bus (RMB).
This document will always be
work in progress. Read the official docs for updates.
Module Structure
In Rust there are multiple ways to create a (sub)module in your crate
<module>.rsA single file module. can be imported inmain.rsorlib.rswith the keywordmod<module>/mod.rsA directory module. Uses mod.rs as the moduleentrypointalways. Other sub-modules can be created next to mod.rs and can be made available by using themodkeyword again in themod.rsfile.
We will agree to use the 2nd way (directory) but with the following restrictions:
mod.rswill have alltraitsandconcrete typesused by the traits.<implementation>.rsfile next tomod.rsthat can include implementation for the module trait. z
Example
Following is an example of animal module.
animal/
mod.rs
dog.rs
cat.rs
File names are always in
snake_casebut avoid the_as much as possible because they basically look ugly in file tree. For example we prefer the namedog.rsoverdog_animal.rsbecause we already can tell from the module name that it's adoganimal. Hence in the identity module for example the nameed25519.rsis preferred overed25519_identity.rsbecause that's already inferred from the module name.
The mod.rs file then can contain
pub mod dog;
pub mod cat;
pub use dog::Dog;
pub trait Food {
fn calories(&self) -> u32;
}
pub trait Animal<F>
where
F: Food,
{
fn feed(&mut self, food: F);
}
The dog.rs file then can contain
use super::{Animal, Food};
pub struct DogFood {}
impl Food for DogFood {
fn calories(&self) -> u32 {
1000
}
}
pub struct Dog {}
impl Animal<DogFood> for Dog {
fn feed(&mut self, food: DogFood) {
println!("yum yum yum {} calories", food.calories());
}
}
A user of the module now can do
use animal::dog::{Dog, DogFood};
For common implementation that are usually used in your modules, a pub use can be added in mod.rs to make it easier to import your type. For example
// dog is brought directly from animal crate
use animal::Dog;
// cat i need to get from the sub-module
use animal::cat::Cat;
Naming Conventions
Following the rust guide lines for name
file namesare short snake case. avoid_if otherwise name will not be descriptive. Check note about file names above.trait,struct,enumnames are allCamelCasefn,variablesnames are snake case
Note, names of functions and variables need to be descriptive but short at the same time. Also avoid the _ until absolutely necessary. A variable with a single word name is better if it doesn't cause confusion with other variables in the same context.
The name of the variable should never include the type.
error Handling
We agreed to use anyhow crate in this project. Please read the docs for anyhow
To unify the practice by default we import both Result and Context from anyhow
Others can be imported as well if needed.
use anyhow::{Result, Context};
fn might_fail() -> Result<()> {
// context adds a `context` to the error. so if another_call fails. I can tell exactly failed when i was doing what
another_call().context("failed to do something")?; // <- we use ? to propagate the error unless you need to handle the error differently
Ok(()) // we use Ok from std no need to import anyhow::Ok although it's probably the same.
}
fn might_fail2() -> Result<()> {
if fail {
// use the bail macro fom anyhow to exit with an error.
bail!("failed because fail with set to true");
}
}
> NOTE: all error messages starts with lowercase. for example it's `failed to ...` not `Failed to ...`
logs
logging is important to trace the errors that cannot be propagated and also for debug messages that can help spotting a problem. We always gonna use log crate. as
log::debug!(); // for debug messages
log::info!(); // info messages
Note only errors that can NOT be propagated are logged.
NOTE: All log messages start with lowercase.
Function Signatures
For function inputs (arguments) generic types are preferred if available over concrete types. This most obvious with string types. depending on the function behavior
Examples
This is bad:
fn call1(key: String);
fn call2(key: &str);
It is preferred to use:
// in case function will need to take ownership of the string.
fn call1<k: Into<String>>(k: K);
// inc ase function will just need to use a reference to the string.
fn call2<K: AsRef<str>>(k: K);
// this will allow both functions to be callable with `&str`, `String`.