Project-Specific Patterns
- Use the 2024 edition of Rust
- Prefer
derive_moreover manual trait implementations - Feature flags in this codebase use the
#[cfg(feature = "...")]pattern - Invoke
cargo clippywith--all-features,--all-targets, and--no-depsfrom the root - Use
cargo doc --no-deps --all-featuresfor checking documentation - Use
rustfmtto format the code - Use
#[expect(lint, reason = "...")]over#[allow(lint)]
Type System
- Create strong types with newtype patterns for domain entities
- Consider visibility carefully (avoid unnecessary
pub)
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, derive_more::Display)]
pub struct UserId(Uuid);
Async Patterns
- Use
impl Future<Output = T> + Sendin trait definitions:
fn get_data(
&self,
id: String,
) -> impl Future<Output = Result<Data, Report<DataError>>> + Send {
async move {
// Implementation
}
}
Function Arguments
- Functions should never take more than 7 arguments. If a function requires more than 7 arguments, encapsulate related parameters in a struct.
- Functions that use data immutably should take a reference to the data, while functions that modify data should take a mutable reference. Never take ownership of data unless the function explicitly consumes it.
- Make functions
constwhenever possible. - Prefer the following argument types when applicable, but only if this does not reduce performance:
impl AsRef<str>instead of&stror&Stringimpl AsRef<Path>instead of&Pathor&PathBufimpl IntoIterator<Item = &T>when only iterating over the data&[T]instead of&Vec<T>&mut [T]instead of&mut Vec<T>when the function doesn't need to resize the vectorimpl Into<Cow<T>>instead ofCow<T>impl Into<Arc<T>>instead ofArc<T>impl Into<Rc<T>>instead ofRc<T>impl Into<Box<T>>instead ofBox<T>
- Never use
impl Into<Option<_>>as from reading the caller site, it's not visible thatNonecould potentially be passed
From and Into
- Generally prefer
Fromimplementations overIntoimplementations. The Rust compiler will automatically deriveIntofromFrom, but not vice versa. - When converting between types, prefer using the
frommethod overintofor clarity. Thefrommethod makes the target type explicit in the code, whileintorequires type inference. - For wrapper types like
Cow,Arc,Rc,Report, andBox, prefer using explicit constructors (e.g.,Cow::from,Arc::new) instead of.into(). This improves readability by clearly indicating the target type.
Smart Pointers
- When cloning smart pointers such as
ArcandRc, always useArc::clone(&pointer)andRc::clone(&pointer)instead ofpointer.clone(). This explicitly indicates you're cloning the reference, not the underlying data.
Instrumentation
- Annotate functions that perform significant work with
#[tracing::instrument] - Use
tracingmacros (e.g.,trace!,debug!,info!,warn!,error!) instead ofprintln!oreprintln!for logging
Allocations
- Minimize allocations when possible. For example, reuse a
Vecin a loop instead of creating a new one in each iteration. - Prefer borrowed data over owned data where appropriate.
- Balance performance and readability—if an allocation makes code significantly more readable or maintainable, the trade-off may be worthwhile.
Types
- Use newtypes when a value should carry specific semantics beyond its underlying type. This improves type safety and code clarity.
For example:
struct UserId(u64); // instead of `type UserId = u64;` or `u64`
Naming Conventions
When suggesting names for variables, functions, or types:
- Do not prefix test-function names with
test_, this would otherwise result intest::test_<name>names. - Provide a concise list of naming options with brief explanations of why each fits the context
- Choose names of appropriate length—avoid names that are too long or too short
- Avoid abbreviations unless they are widely recognized in the domain (e.g.,
HttporJsonis acceptable, butCtxinstead ofContextis not) - Do not suffix names with their types (e.g., use
usersinstead ofusersList) - Do not repeat the type name in variable names (e.g., use
userinstead ofuserUser)
Crate Preferences
- Use
similar_assertsfor test assertions - Use
instafor snapshot tests - Use
test_logfor better test output (#[test_log::test]) - Use
tracingmacros, notlogmacros - Prefer
tracing::instrumentfor function instrumentation
Import Style
- Don't use local imports within functions, or blocks
- Avoid wildcard imports like
use super::*;, oruse crate::module::*; - Never use a prelude
use crate::prelude::* - Prefer explicit imports to make dependencies clear and improve code readability
- We prefer
coreoverallocoverstdfor imports to minimize dependencies- Use
corefor functionality that doesn't require allocation - Use
allocwhen you need allocation but not OS-specific features - Only use
stdwhen necessary for OS interactions or when usingcore/allocwould be unnecessarily complex
- Use
- Prefer qualified imports (
use foo::Bar; let x = Bar::new()) over fully qualified paths (let x = foo::Bar::new()) for frequently used types - Use
pub usere-exports in module roots to create a clean public API - Avoid importing items with the same name from different modules; use qualified imports
- Import traits using
use module::Trait as _;when you only need the trait's methods and not the trait name itself- This pattern brings trait methods into scope without name conflicts
- Use this especially for extension traits or when implementing foreign traits on local types
// Good - Importing a trait just for its methods:
use std::io::Read as _;
// Example with trait methods:
fn read_file(file: &mut File) -> Result<String, std::io::Error> {
// Read methods available without importing the Read trait name
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
// Bad - Directly importing trait when only methods are needed:
use std::io::Read;
// Good - Importing trait for implementing it:
use std::io::Write;
impl Write for MyWriter { /* implementation */ }
// Bad - Wildcard import:
mod tests {
use super::*; // Wildcard import
#[test]
fn test_something() {
// Test implementation
}
}
// Good - Explicit imports:
mod tests {
use crate::MyStruct;
use crate::my_function;
#[test]
fn test_something() {
// Test implementation
}
}
// Bad - Local import:
fn process_data() {
use std::collections::HashMap; // Local import
let map = HashMap::new();
// Implementation
}
// Good - Module-level import:
use std::collections::HashMap;
fn process_data() {
let map = HashMap::new();
// Implementation
}
// Bad - Using std when core would suffice:
use std::fmt::Display;
// Good - Using core for non-allocating functionality:
use core::fmt::Display;
// Bad - Using std when alloc would suffice:
use std::collections::BTreeSet;
// Good - Using alloc for allocation without full std dependency:
use alloc::vec::Vec;
// Appropriate - Using std when needed:
use std::fs::File; // OS-specific functionality requires std
Libraries and Components
- Abstract integrations with third-party systems behind traits to maintain clean separation of concerns
Comments and Assertions
- Do not add comments after a line of code; place comments on separate lines above the code they describe
- When using assertions, include descriptive messages using the optional description parameter rather than adding a comment
- All
expect()messages should follow the format "should ..." to clearly indicate the expected behavior
For example:
// Bad:
assert_eq!(result, expected); // This should match the expected value
// Good:
assert_eq!(result, expected, "Values should match expected output");
// Bad:
some_value.expect("The value is not None"); // This should never happen
// Good:
some_value.expect("should contain a valid value");