Nazara Code Style Guide
General Guide
In regards to formatting, we follow the current Rust code style set by the Cargo formatter (cargo fmt
).
The easiest way to enforce this, is using pre-commit
to automatically format and check any code before it is committed.
This style is enforced by our CI as well. PRs will not be accepted unless the formatting issues are fixed.
Panics
We heavily discourage the use of the panic!()
macro when aborting the process. Instead, we prefer to handle an error
and abort gracefully with a helpful error message and error code.
We only want to panic, when the issue we encounter is both catastrophic and unfixable by the user.
These are szenarios in which we don't want to panic:
- When input or config parameters are missing
- When connection to NetBox fails
- When an API request returns a different error code that Ok
While we want to panic in these cases:
- When filesystem operations fail
- When Nazara is not run as root
Instead of panicking, we write custom error types to wrap errors of certain functions and include a well formatted error message alongside the error.
For an exampel on how that looks, please check and of the error.rs
files found in any of Nazara's packages.
Example:src/publisher/error.rs
Example:src/publisher/error.rs
In this example, you can see how we write our custom error types and are able to wrap errors that we receive from other functions in order to escalate them properly.
#![allow(unused)] fn main() { //! This module provides error types for the config parser. use std::io::Error as IoError; use std::{error::Error, fmt}; use toml::{de::Error as TomlDeError, ser::Error as TomlSeError}; /// All errors the config parser can encounter. #[derive(Debug)] pub enum ConfigError { /// Used for handling errors during file operations. FileOpError(IoError), /// Indicates that no config file has been found, or it has been moved or deleted during program startup. NoConfigFileError(String), /// Indicates that a required config option is missing from the config file. MissingConfigOptionError(String), /// Indicates an error during deserialization of the TOML config file. DeserializationError(TomlDeError), /// Indicates an error during Serialization of config parameters to a TOML value. SerializationError(TomlSeError), /// Expects a `String` message. Used for edge cases and general purpose error cases. Other(String), } impl fmt::Display for ConfigError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ConfigError::FileOpError(err) => { write!(f, "\x1b[31m[error]\x1b[0m File operation failed: {err}") } ConfigError::NoConfigFileError(err) => { write!(f, "\x1b[31m[error]\x1b[0m No config file found: {err}") } ConfigError::MissingConfigOptionError(err) => { write!( f, "\x1b[31m[error]\x1b[0m Missing required config parameter: {err}" ) } ConfigError::DeserializationError(err) => { write!(f, "\x1b[31m[error]\x1b[0m Invalid config file: {err}") } ConfigError::SerializationError(err) => { write!(f, "\x1b[31m[error]\x1b[0m Serialization error: {err}") } ConfigError::Other(err) => { write!(f, "\x1b[31m[error]\x1b[0m Config Parser error: {err}") } } } } impl Error for ConfigError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { ConfigError::FileOpError(err) => Some(err), ConfigError::NoConfigFileError(_) => None, ConfigError::MissingConfigOptionError(_) => None, ConfigError::DeserializationError(err) => Some(err), ConfigError::SerializationError(err) => Some(err), ConfigError::Other(_) => None, } } } }
Focus on Readability
When writing code, we value readability above complexity.
Rust offers a lot of possibilities when it comes to reducing code down into a one-line statement. And that's nice, and sometimes even necessary.
However, we want to take a readability first approach with our codebase. Both to aid maintainability and increase accessibility for newcomers.
Of course, sometimes using Rust's more "advanced" features will drastically improve performance. So a balance has to be struck between readability and performance. It is very hard to define a solid policy for this, so this is a decision every developer and contributor has to do for themselves and - when necessary - engage in discussion with maintainers, devs and other contributors about their approach and possible alternatives.
Documenting Code
For a smilar reason, we encourage devs to properly document their contributions. This includes but is not limited to:
- Using inline comments to explain possibly hard to understand syntax
- Using Rust's powerful docstring feature to properly document functions, structs and modules
- Adding to this documentation when applicable
- Filling out Issue and PR templates appropriately to aid maintainers review their changes
The following examples will show you an ideal docstring style.
Example: Documenting Functions
Example: Documenting Functions
Documenting Functions:
#![allow(unused)] fn main() { /// This function does X. /// /// This function does X by doing Y using Z. (Detailed explanation optional) /// /// # Parameters /// * `arg: str` - A string argument to process /// /// # Returns /// * `Ok(str)` - Returns A, if ... /// * `Err` - Returns an `ErrType` pub fn foo(arg: str) -> Result<str, Err> { // ... } }
While not universally used by all projects, we use # Parameters
and # Returns
sections, especially for larger functions.
If a function does not take arguments, the # Parameters
section can be omitted.
However, if the function does not return (!
type) - or returns ()
, this has to be indicated
in the # Returns
section.
Other sections we use are:
# Aborts
- If the function aborts the program. (E.g When input parameters are missing)# Panics
- If the function can cause apanic!()
.
For both of these sections, list all - or at least the most common - reasons this behaviour can occur. This can help debugging immensely.
#![allow(unused)] fn main() { /// This function does X. /// /// # Paramters /// - `path: &str` - The path to a file /// /// # Returns /// - `String` - The contents of the file. /// /// # Aborts /// This function will exit the process if the file cannot be found. pub fn read_file(path: &str) -> String { match fs::read_to_string(path) { Ok(contents) => contents, Err(err) => { eprintln!("[Error] File '{}' does not exist: {}", path, err); process::exit(1); } } } }
Example: Documenting Structs
Example: Documenting Structs
Documenting Structs:
#![allow(unused)] fn main() { /// Information about a Person. pub struct Person { /// The name of the Person. pub name: String, /// The age of the Person. pub age: i64, } }
It is encouraged to briefly document every field of your struct, whether they are pub
or not does not matter.
Example: Documenting Modules
Example: Documenting Modules
Documenting Modules:
#![allow(unused)] fn main() { //! This module handles X. }
You can go into depth here about what a module does. This is encouraged for larger modules.
Please be aware, that a one-liner docstring above a function will not suffice. Maintainers may ask you to stick to the given format for your contribution.
We are aware that to some of you, this feels like cluttering the code files. However, we believe that properly documenting our code is the key to providing a more inclusive and more maintainable development experience for all.
Terminal Output/User Interface
When it is necessary to inform the user about a process, we want to make it short, but as expressive as possible. For this, the following styles apply:
Process X has been started...
- Basic white text indicates the current process.\x1b[32m[success]\x1b[0m Something has succeeded
- Green colored[success]
prefix before the message.\x1b[31m[error]\x1b[0m Something has failed!
- Red colored[error]
prefix before error message. This should automatically be added by our custom error types when they are given a error message.\αΊ‹1b[36m[info]\x1b[0m Information level message.
- Light blue colored[info]
prefix.\x1b[33m[warning]\x1b[0m Something went wrong, but we can continue...
- Yellow colored[warning]
prefix.
Always remember to close the coloring by resetting the color using the \x1b[0m
control sequence.