info_tfgrid/collections/developers/internals/rmb/rmb_coding_guides.md

174 lines
5.1 KiB
Markdown
Raw Normal View History

2024-06-13 19:50:44 +00:00
<h1> Coding Guides </h1>
<h2> Table of Contents </h2>
- [Introduction](#introduction)
- [Module Structure](#module-structure)
- [Example](#example)
- [Naming Conventions](#naming-conventions)
- [`error` Handling](#error-handling)
- [`logs`](#logs)
- [Function Signatures](#function-signatures)
- [Examples](#examples)
---
## 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](https://github.com/threefoldtech/rmb-rs/blob/main/docs/code-guide.md).
## Module Structure
In Rust there are multiple ways to create a (sub)module in your crate
- `<module>.rs` A single file module. can be imported in `main.rs` or `lib.rs` with the keyword `mod`
- `<module>/mod.rs` A directory module. Uses mod.rs as the module `entrypoint` always. Other sub-modules can be created next to mod.rs and can be made available by using the `mod` keyword again in the `mod.rs` file.
We will agree to use the 2nd way (directory) but with the following restrictions:
- `mod.rs` will have all `traits` and `concrete types` used by the traits.
- `<implementation>.rs` file next to `mod.rs` that 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_case` but avoid the `_` as much as possible because they basically look ugly in file tree. For example we prefer the name `dog.rs` over `dog_animal.rs` because we already can tell from the module name that it's a `dog` __animal__. Hence in the identity module for example the name `ed25519.rs` is preferred over `ed25519_identity.rs` because that's already inferred from the module name.
The `mod.rs` file then can contain
```rust
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
```rust
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
```rust
// 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 names` are short snake case. avoid `_` if otherwise name will not be descriptive. Check note about file names above.
- `trait`, `struct`, `enum` names are all `CamelCase`
- `fn`, `variables` names 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`](https://docs.rs/anyhow/1.0.57/anyhow/)
To unify the practice by default we import both `Result` and `Context` from `anyhow`
> Others can be imported as well if needed.
```rust
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
```rust
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:
```rust
fn call1(key: String);
fn call2(key: &str);
```
It is preferred to use:
```rust
// 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`.
```