Rust Development Guidelines - OpenEV Data API¶
This document establishes the coding standards, best practices, and architectural guidelines for developing the OpenEV Data API in Rust. All contributors must follow these guidelines to ensure code quality, maintainability, and consistency.
Table of Contents¶
- 1. Core Principles
- 2. Project Structure
- 3. Naming Conventions
- 4. Type Design
- 5. Error Handling
- 6. Architectural Patterns
- 7. Async/Await Patterns
- 8. Testing Strategy
- 9. Performance Guidelines
- 10. Security Practices
- 11. Documentation Standards
- 12. Code Quality Tools
- 13. Common Patterns
- 14. Anti-Patterns to Avoid
1. Core Principles¶
1.1. SOLID Principles in Rust¶
Single Responsibility Principle (SRP)¶
Each module, struct, and function should have ONE clear responsibility.
Good:
// Each struct has a single responsibility
pub struct VehicleReader {
// Only reads vehicle files
}
pub struct VehicleValidator {
// Only validates vehicle data
}
pub struct VehicleRepository {
// Only handles vehicle persistence
}
Bad:
// This struct does too many things
pub struct VehicleManager {
// Reads, validates, transforms, saves, queries...
}
Open/Closed Principle (OCP)¶
Types should be open for extension but closed for modification. Use traits for extensibility.
Good:
// Define behavior through traits
pub trait OutputGenerator {
fn generate(&self, vehicles: &[Vehicle]) -> Result<Vec<u8>>;
}
// Easy to add new formats without modifying existing code
pub struct JsonGenerator;
impl OutputGenerator for JsonGenerator { /* ... */ }
pub struct SqliteGenerator;
impl OutputGenerator for SqliteGenerator { /* ... */ }
Liskov Substitution Principle (LSP)¶
Implementations should be substitutable for their trait bounds without breaking behavior.
Good:
pub trait VehicleStore {
fn save(&mut self, vehicle: Vehicle) -> Result<()>;
fn find(&self, id: &str) -> Result<Option<Vehicle>>;
}
// Both implementations respect the contract
impl VehicleStore for SqliteStore { /* ... */ }
impl VehicleStore for PostgresStore { /* ... */ }
Interface Segregation Principle (ISP)¶
Clients should not depend on traits they don't use. Keep traits focused.
Good:
// Segregated traits
pub trait Readable {
fn read(&self, id: &str) -> Result<Vehicle>;
}
pub trait Writable {
fn write(&mut self, vehicle: Vehicle) -> Result<()>;
}
// Implement only what you need
impl Readable for ReadOnlyStore { /* ... */ }
impl Readable + Writable for FullStore { /* ... */ }
Bad:
// Fat trait forcing unnecessary implementations
pub trait Repository {
fn read(&self, id: &str) -> Result<Vehicle>;
fn write(&mut self, vehicle: Vehicle) -> Result<()>;
fn delete(&mut self, id: &str) -> Result<()>;
fn update(&mut self, vehicle: Vehicle) -> Result<()>;
}
Dependency Inversion Principle (DIP)¶
Depend on abstractions (traits), not concrete types.
Good:
// Depend on trait, not concrete type
pub struct EtlPipeline<G: OutputGenerator> {
generator: G,
}
impl<G: OutputGenerator> EtlPipeline<G> {
pub fn new(generator: G) -> Self {
Self { generator }
}
}
Bad:
// Hardcoded dependency on concrete type
pub struct EtlPipeline {
generator: JsonGenerator, // Tightly coupled
}
1.2. Hexagonal Architecture Enforcement¶
The codebase is divided into layers with strict dependency rules:
┌─────────────────────────────────────┐
│ Adapters (Outer) │
│ (ev-etl, ev-server) │
│ ↓ depends on │
├─────────────────────────────────────┤
│ Core (Inner) │
│ (ev-core) │
│ ← no dependencies │
└─────────────────────────────────────┘
Rules: 1. ev-core must NOT depend on ev-etl or ev-server 2. ev-core must NOT import any I/O, HTTP, or database crates 3. ev-etl and ev-server can depend on ev-core 4. Adapters communicate with Core through trait boundaries
1.3. Make Invalid States Unrepresentable¶
Use Rust's type system to prevent invalid states at compile time.
Good:
// Impossible to have a vehicle without required fields
pub struct Vehicle {
pub make: SlugName, // Cannot be empty
pub model: SlugName,
pub year: Year, // Validated range (1900-2100)
pub battery: BatterySpecs, // At least one capacity required
}
// Year is constrained
pub struct Year(u16);
impl Year {
pub fn new(value: u16) -> Result<Self, ValidationError> {
if (1900..=2100).contains(&value) {
Ok(Year(value))
} else {
Err(ValidationError::InvalidYear(value))
}
}
}
Bad:
// Many ways to be invalid
pub struct Vehicle {
pub make: Option<String>, // Could be None
pub model: Option<String>,
pub year: i32, // Could be negative or 9999
pub battery: Option<Battery>, // Could be None
}
2. Project Structure¶
2.1. Workspace Organization¶
Cargo.toml # Workspace manifest
├── crates/
│ ├── ev-core/ # Domain logic (no dependencies)
│ │ ├── src/
│ │ │ ├── lib.rs # Public API surface
│ │ │ ├── domain/ # Domain entities
│ │ │ ├── validation/ # Validation logic
│ │ │ └── error.rs # Core error types
│ │ └── Cargo.toml
│ │
│ ├── ev-etl/ # CLI adapter
│ │ ├── src/
│ │ │ ├── main.rs # Binary entry
│ │ │ ├── cli.rs # Argument parsing
│ │ │ ├── ingest/ # Input adapters
│ │ │ ├── output/ # Output adapters
│ │ │ └── error.rs # ETL-specific errors
│ │ └── Cargo.toml
│ │
│ └── ev-server/ # HTTP API adapter
│ ├── src/
│ │ ├── main.rs # Binary entry
│ │ ├── api/ # HTTP handlers
│ │ ├── db/ # Database adapters
│ │ └── error.rs # Server-specific errors
│ └── Cargo.toml
2.2. Module Organization¶
Each crate should follow this internal structure:
// lib.rs or main.rs - Define public API
pub mod domain;
pub mod validation;
pub use domain::{Vehicle, Battery}; // Re-export public types
pub use validation::Validator;
// Private modules
mod internal;
2.3. File Naming¶
- snake_case for file names:
vehicle_repository.rs - mod.rs for module entry points
- One public type per file (generally)
- Related types can share a file if small
3. Naming Conventions¶
3.1. General Rules¶
| Item | Convention | Example |
|---|---|---|
| Crates | kebab-case | ev-core, ev-etl |
| Modules | snake_case | vehicle_store, json_parser |
| Types | PascalCase | Vehicle, BatterySpecs |
| Traits | PascalCase | OutputGenerator, Validator |
| Functions | snake_case | load_vehicles(), validate_schema() |
| Methods | snake_case | .to_json(), .save() |
| Constants | SCREAMING_SNAKE_CASE | MAX_VEHICLES, DEFAULT_PORT |
| Statics | SCREAMING_SNAKE_CASE | GLOBAL_CONFIG |
| Lifetimes | lowercase, short | 'a, 'b, 'static |
| Type parameters | PascalCase, single letter | T, E, or descriptive: TOutput |
3.2. Semantic Naming¶
Types should indicate their purpose:
// Good - Clear intent
pub struct VehicleRepository;
pub struct JsonSerializer;
pub struct ValidationError;
// Bad - Vague or generic
pub struct Manager;
pub struct Handler;
pub struct Data;
Functions should be verbs or verb phrases:
// Good
fn load_dataset() -> Result<Dataset>;
fn validate_vehicle(v: &Vehicle) -> Result<()>;
fn merge_json_files(files: &[File]) -> Json;
// Bad
fn dataset() -> Result<Dataset>;
fn vehicle(v: &Vehicle) -> Result<()>;
Avoid abbreviations unless widely known:
// Good
http_client, database_url, repository
// Acceptable
url, json, xml, html, api
// Bad
veh, db_repo, usr
4. Type Design¶
4.1. Prefer Newtype Pattern for Domain Concepts¶
Wrap primitive types to add semantic meaning and validation.
// Good - Type-safe and self-documenting
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VehicleId(String);
impl VehicleId {
pub fn new(value: String) -> Result<Self, ValidationError> {
if value.is_empty() {
return Err(ValidationError::EmptyId);
}
Ok(VehicleId(value))
}
}
// Bad - Easy to confuse different strings
fn get_vehicle(id: String) -> Vehicle;
fn get_make(make: String) -> Vec<Vehicle>;
// Which string is which?
4.2. Use Builder Pattern for Complex Construction¶
#[derive(Debug)]
pub struct VehicleBuilder {
make: Option<SlugName>,
model: Option<SlugName>,
year: Option<Year>,
battery: Option<BatterySpecs>,
}
impl VehicleBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn make(mut self, make: SlugName) -> Self {
self.make = Some(make);
self
}
pub fn build(self) -> Result<Vehicle, BuilderError> {
Ok(Vehicle {
make: self.make.ok_or(BuilderError::MissingMake)?,
model: self.model.ok_or(BuilderError::MissingModel)?,
year: self.year.ok_or(BuilderError::MissingYear)?,
battery: self.battery.ok_or(BuilderError::MissingBattery)?,
})
}
}
4.3. Enums for State Representation¶
Use enums to represent mutually exclusive states.
// Good - Clear states
#[derive(Debug, Clone, PartialEq)]
pub enum VehicleAvailability {
Production { start_year: u16 },
Discontinued { end_year: u16 },
Concept,
Announced { expected_year: u16 },
}
// Bad - Multiple booleans
pub struct VehicleAvailability {
pub is_production: bool,
pub is_discontinued: bool,
pub is_concept: bool,
// Unclear which combination is valid
}
4.4. Derive Standard Traits¶
Always derive standard traits when possible:
#[derive(Debug, Clone, PartialEq, Eq, Hash)] // For most types
#[derive(Debug, Clone, PartialEq)] // For types with floats
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] // For small copyable types
For public domain types, also derive:
5. Error Handling¶
5.1. Error Design¶
Use thiserror for library errors (ev-core) and anyhow for application errors (ev-etl, ev-server).
ev-core (library errors):
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ValidationError {
#[error("Missing required field: {field}")]
MissingField { field: String },
#[error("Invalid year: {0}. Must be between 1900 and 2100")]
InvalidYear(u16),
#[error("At least one battery capacity (gross or net) is required")]
MissingBatteryCapacity,
#[error("Schema validation failed: {0}")]
SchemaError(String),
}
ev-etl, ev-server (application errors):
use anyhow::{Context, Result};
pub fn load_dataset(path: &Path) -> Result<Dataset> {
let file = File::open(path)
.context(format!("Failed to open dataset file: {:?}", path))?;
let dataset = serde_json::from_reader(file)
.context("Failed to parse JSON")?;
Ok(dataset)
}
5.2. Result Type Usage¶
Always use Result for operations that can fail:
// Good
pub fn validate_vehicle(vehicle: &Vehicle) -> Result<(), ValidationError>;
pub fn save_to_db(&self, vehicle: Vehicle) -> Result<VehicleId>;
// Bad - panicking in library code
pub fn validate_vehicle(vehicle: &Vehicle) {
assert!(!vehicle.make.is_empty()); // NO!
}
// Bad - hiding errors
pub fn save_to_db(&self, vehicle: Vehicle) -> Option<VehicleId>;
5.3. Error Propagation¶
Use ? operator for error propagation:
pub fn process_vehicle(path: &Path) -> Result<Vehicle> {
let raw_json = std::fs::read_to_string(path)?;
let vehicle: Vehicle = serde_json::from_str(&raw_json)?;
vehicle.validate()?;
Ok(vehicle)
}
5.4. Never Panic in Libraries¶
Libraries (ev-core) should NEVER panic. Applications can panic for unrecoverable errors.
// Library code - NO PANIC
pub fn parse_year(s: &str) -> Result<Year, ParseError> {
let value: u16 = s.parse()
.map_err(|_| ParseError::InvalidFormat)?;
Year::new(value)
}
// Application code - panic is acceptable for truly unrecoverable errors
pub fn main() {
let config = load_config()
.expect("Configuration file is required");
}
6. Architectural Patterns¶
6.1. Port and Adapter Pattern¶
Define traits (ports) in ev-core, implement them (adapters) in outer crates.
ev-core (port definition):
// Port - abstract interface
pub trait VehicleRepository {
fn find_by_id(&self, id: &VehicleId) -> Result<Option<Vehicle>>;
fn save(&mut self, vehicle: Vehicle) -> Result<VehicleId>;
fn list_all(&self) -> Result<Vec<Vehicle>>;
}
ev-server (adapter implementation):
// Adapter - concrete implementation
pub struct SqliteVehicleRepository {
connection: rusqlite::Connection,
}
impl VehicleRepository for SqliteVehicleRepository {
fn find_by_id(&self, id: &VehicleId) -> Result<Option<Vehicle>> {
// SQLite-specific implementation
}
fn save(&mut self, vehicle: Vehicle) -> Result<VehicleId> {
// SQLite-specific implementation
}
fn list_all(&self) -> Result<Vec<Vehicle>> {
// SQLite-specific implementation
}
}
6.2. Dependency Injection¶
Use constructor injection with trait bounds:
// Service depends on abstraction, not implementation
pub struct EtlPipeline<R, G>
where
R: VehicleRepository,
G: OutputGenerator,
{
repository: R,
generator: G,
}
impl<R, G> EtlPipeline<R, G>
where
R: VehicleRepository,
G: OutputGenerator,
{
pub fn new(repository: R, generator: G) -> Self {
Self { repository, generator }
}
pub fn process(&mut self, vehicles: Vec<Vehicle>) -> Result<Vec<u8>> {
for vehicle in vehicles {
self.repository.save(vehicle)?;
}
let all_vehicles = self.repository.list_all()?;
self.generator.generate(&all_vehicles)
}
}
6.3. Strategy Pattern¶
Use traits to define interchangeable algorithms:
pub trait MergeStrategy {
fn merge(&self, base: &Json, overlay: &Json) -> Json;
}
pub struct DeepMergeStrategy;
impl MergeStrategy for DeepMergeStrategy {
fn merge(&self, base: &Json, overlay: &Json) -> Json {
// Deep merge implementation
}
}
pub struct ShallowMergeStrategy;
impl MergeStrategy for ShallowMergeStrategy {
fn merge(&self, base: &Json, overlay: &Json) -> Json {
// Shallow merge implementation
}
}
6.4. Repository Pattern¶
Separate data access from business logic:
// Repository handles persistence
pub struct VehicleRepositoryImpl {
db: Database,
}
impl VehicleRepositoryImpl {
fn to_domain(&self, row: DbRow) -> Result<Vehicle> {
// Map database row to domain entity
}
fn from_domain(&self, vehicle: &Vehicle) -> DbRow {
// Map domain entity to database row
}
}
7. Async/Await Patterns¶
7.1. When to Use Async¶
Use async for: - HTTP requests/responses (ev-server) - Database connections with async drivers - Concurrent I/O operations
Don't use async for: - Pure computation - ev-core (domain logic should be sync) - ETL processing (unless parallelizing I/O)
7.2. Async Trait Methods¶
Use async-trait crate for trait methods:
use async_trait::async_trait;
#[async_trait]
pub trait AsyncVehicleRepository {
async fn find_by_id(&self, id: &VehicleId) -> Result<Option<Vehicle>>;
async fn save(&self, vehicle: Vehicle) -> Result<VehicleId>;
}
#[async_trait]
impl AsyncVehicleRepository for PostgresRepository {
async fn find_by_id(&self, id: &VehicleId) -> Result<Option<Vehicle>> {
// Async implementation
}
async fn save(&self, vehicle: Vehicle) -> Result<VehicleId> {
// Async implementation
}
}
7.3. Concurrent Processing¶
Use tokio::spawn for true parallelism, futures::join! for concurrent awaiting:
use futures::future::join_all;
pub async fn load_multiple_vehicles(ids: Vec<VehicleId>) -> Result<Vec<Vehicle>> {
let futures = ids
.into_iter()
.map(|id| async move {
load_vehicle(&id).await
});
join_all(futures).await
.into_iter()
.collect::<Result<Vec<_>>>()
}
7.4. Tokio Runtime Selection¶
// For servers - multi-threaded runtime
#[tokio::main]
async fn main() -> Result<()> {
// Server code
}
// For CLIs - current thread runtime (lighter)
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
// CLI code
}
8. Testing Strategy¶
8.1. Test Organization¶
src/
vehicle.rs
vehicle_repository.rs
tests/ # Integration tests
vehicle_integration_test.rs
benches/ # Benchmarks
vehicle_benchmark.rs
8.2. Unit Tests¶
Place unit tests in the same file as the code:
pub struct Vehicle {
// ...
}
impl Vehicle {
pub fn is_available_in_year(&self, year: u16) -> bool {
// implementation
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vehicle_availability() {
let vehicle = Vehicle::new(/* ... */);
assert!(vehicle.is_available_in_year(2024));
assert!(!vehicle.is_available_in_year(1999));
}
#[test]
fn test_invalid_year_returns_error() {
let result = Year::new(3000);
assert!(result.is_err());
}
}
8.3. Integration Tests¶
Place integration tests in tests/ directory:
// tests/etl_pipeline_test.rs
use ev_etl::EtlPipeline;
use ev_core::Vehicle;
#[test]
fn test_full_etl_pipeline() {
let input_dir = "fixtures/sample_vehicles";
let output_dir = temp_dir();
let result = EtlPipeline::new()
.input(input_dir)
.output(output_dir)
.formats(vec!["json", "sqlite"])
.run();
assert!(result.is_ok());
assert!(output_dir.join("vehicles.json").exists());
assert!(output_dir.join("vehicles.db").exists());
}
8.4. Property-Based Testing¶
Use proptest for property-based testing:
use proptest::prelude::*;
proptest! {
#[test]
fn test_year_roundtrip(year in 1900u16..=2100u16) {
let y = Year::new(year).unwrap();
let json = serde_json::to_string(&y).unwrap();
let parsed: Year = serde_json::from_str(&json).unwrap();
prop_assert_eq!(y, parsed);
}
}
8.5. Mocking¶
Use mockall for mocking traits:
use mockall::{automock, predicate::*};
#[automock]
pub trait VehicleRepository {
fn find_by_id(&self, id: &VehicleId) -> Result<Option<Vehicle>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_with_mock_repository() {
let mut mock_repo = MockVehicleRepository::new();
mock_repo
.expect_find_by_id()
.with(eq(VehicleId::new("test".to_string()).unwrap()))
.times(1)
.returning(|_| Ok(Some(create_test_vehicle())));
let service = VehicleService::new(mock_repo);
let result = service.get_vehicle_details(&VehicleId::new("test".to_string()).unwrap());
assert!(result.is_ok());
}
}
8.6. Test Utilities¶
Create test helpers in a common module:
// tests/common/mod.rs
#![allow(dead_code)]
use ev_core::*;
pub fn create_test_vehicle() -> Vehicle {
VehicleBuilder::new()
.make(SlugName::new("test_make", "Test Make").unwrap())
.model(SlugName::new("test_model", "Test Model").unwrap())
.year(Year::new(2024).unwrap())
.battery(create_test_battery())
.build()
.unwrap()
}
pub fn create_test_battery() -> BatterySpecs {
BatterySpecs {
pack_capacity_kwh_net: Some(60.0),
thermal_management: Some(ThermalManagement::Liquid),
..Default::default()
}
}
// tests/vehicle_test.rs
mod common;
#[test]
fn test_something() {
let vehicle = common::create_test_vehicle();
// ...
}
9. Performance Guidelines¶
9.1. Avoid Unnecessary Allocations¶
// Good - borrows instead of cloning
pub fn process_vehicles(vehicles: &[Vehicle]) -> Vec<String> {
vehicles.iter()
.map(|v| format_vehicle(v))
.collect()
}
// Bad - unnecessary clone
pub fn process_vehicles(vehicles: &[Vehicle]) -> Vec<String> {
vehicles.to_vec() // Clones all vehicles!
.iter()
.map(|v| format_vehicle(v))
.collect()
}
9.2. Use Iterators Instead of Loops¶
// Good - iterator chain
let total_capacity: f64 = vehicles
.iter()
.filter(|v| v.year == 2024)
.filter_map(|v| v.battery.pack_capacity_kwh_net)
.sum();
// Bad - manual loop
let mut total_capacity = 0.0;
for vehicle in vehicles {
if vehicle.year == 2024 {
if let Some(capacity) = vehicle.battery.pack_capacity_kwh_net {
total_capacity += capacity;
}
}
}
9.3. Prefer &str Over String in Function Parameters¶
// Good - accepts both String and &str
pub fn parse_vehicle_id(id: &str) -> Result<VehicleId> {
VehicleId::new(id.to_string())
}
// Bad - forces caller to own a String
pub fn parse_vehicle_id(id: String) -> Result<VehicleId> {
VehicleId::new(id)
}
9.4. Use Cow for Conditional Ownership¶
use std::borrow::Cow;
pub fn normalize_slug(s: &str) -> Cow<str> {
if s.chars().all(|c| c.is_ascii_lowercase() || c == '_') {
Cow::Borrowed(s) // No allocation needed
} else {
Cow::Owned(s.to_lowercase().replace('-', "_")) // Allocate only if needed
}
}
9.5. Parallel Processing with Rayon¶
For CPU-bound work, use rayon:
use rayon::prelude::*;
pub fn validate_all_vehicles(vehicles: Vec<Vehicle>) -> Vec<Result<(), ValidationError>> {
vehicles
.par_iter() // Parallel iterator
.map(|v| v.validate())
.collect()
}
9.6. Benchmarking¶
Use criterion for benchmarking:
// benches/vehicle_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use ev_core::Vehicle;
fn benchmark_vehicle_validation(c: &mut Criterion) {
let vehicle = create_test_vehicle();
c.bench_function("validate_vehicle", |b| {
b.iter(|| {
vehicle.validate()
});
});
}
criterion_group!(benches, benchmark_vehicle_validation);
criterion_main!(benches);
10. Security Practices¶
10.1. Input Validation¶
Always validate untrusted input:
pub fn load_vehicle_from_file(path: &Path) -> Result<Vehicle> {
// Validate path is within expected directory
let canonical_path = path.canonicalize()?;
if !canonical_path.starts_with("/allowed/directory") {
return Err(SecurityError::InvalidPath.into());
}
// Limit file size
let metadata = std::fs::metadata(&canonical_path)?;
if metadata.len() > MAX_FILE_SIZE {
return Err(SecurityError::FileTooLarge.into());
}
// Parse with size limits
let content = std::fs::read_to_string(&canonical_path)?;
serde_json::from_str(&content)
.map_err(|e| e.into())
}
10.2. Avoid SQL Injection¶
Use parameterized queries:
// Good - parameterized
pub fn find_vehicles_by_make(&self, make: &str) -> Result<Vec<Vehicle>> {
let mut stmt = self.conn.prepare(
"SELECT * FROM vehicles WHERE make_slug = ?1"
)?;
stmt.query_map([make], |row| {
// Map row to Vehicle
})
}
// Bad - string concatenation (SQL injection risk)
pub fn find_vehicles_by_make(&self, make: &str) -> Result<Vec<Vehicle>> {
let query = format!("SELECT * FROM vehicles WHERE make_slug = '{}'", make);
// NEVER DO THIS
}
10.3. Secrets Management¶
Never hardcode secrets:
// Good - environment variable
pub fn database_url() -> Result<String> {
std::env::var("DATABASE_URL")
.context("DATABASE_URL environment variable not set")
}
// Bad - hardcoded
pub fn database_url() -> String {
"postgresql://user:password123@localhost/db".to_string()
}
10.4. Dependencies¶
- Review dependencies before adding
- Use
cargo auditregularly - Pin dependency versions in
Cargo.lock - Prefer well-maintained crates
11. Documentation Standards¶
11.1. Module Documentation¶
Every module should have a doc comment:
//! Vehicle repository implementations.
//!
//! This module provides database access for vehicle entities through
//! implementations of the `VehicleRepository` trait.
//!
//! # Examples
//!
//! ```
//! use ev_core::{Vehicle, VehicleRepository};
//!
//! let repo = SqliteVehicleRepository::new("vehicles.db")?;
//! let vehicle = repo.find_by_id(&id)?;
//! ```
pub mod sqlite;
pub mod postgresql;
11.2. Type Documentation¶
Document all public types:
/// Represents an electric vehicle with complete specifications.
///
/// A `Vehicle` contains all the essential information about an EV including
/// manufacturer details, battery specifications, charging capabilities, and
/// performance metrics.
///
/// # Examples
///
/// ```
/// use ev_core::{Vehicle, VehicleBuilder};
///
/// let vehicle = VehicleBuilder::new()
/// .make("tesla", "Tesla")
/// .model("model_3", "Model 3")
/// .year(2024)
/// .build()?;
/// ```
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Vehicle {
/// Vehicle manufacturer information
pub make: SlugName,
/// Vehicle model name and slug
pub model: SlugName,
/// Model year (1900-2100)
pub year: Year,
/// Battery specifications and capabilities
pub battery: BatterySpecs,
}
11.3. Function Documentation¶
Document public functions with examples:
/// Validates a vehicle against the JSON schema and business rules.
///
/// # Errors
///
/// Returns `ValidationError` if:
/// - Required fields are missing
/// - Values are out of valid range
/// - Business rules are violated
///
/// # Examples
///
/// ```
/// let vehicle = create_test_vehicle();
/// assert!(validate_vehicle(&vehicle).is_ok());
/// ```
pub fn validate_vehicle(vehicle: &Vehicle) -> Result<(), ValidationError> {
// Implementation
}
11.4. Error Documentation¶
Document error variants:
/// Errors that can occur during vehicle validation.
#[derive(Debug, Error)]
pub enum ValidationError {
/// A required field is missing from the vehicle data.
///
/// This typically indicates incomplete input data.
#[error("Missing required field: {field}")]
MissingField {
/// The name of the missing field
field: String,
},
/// The year value is outside the valid range (1900-2100).
#[error("Invalid year: {0}. Must be between 1900 and 2100")]
InvalidYear(u16),
}
11.5. README per Crate¶
Each crate should have a README.md:
# ev-core
Core domain types and validation logic for OpenEV Data.
## Features
- Type-safe domain entities
- Comprehensive validation
- Zero I/O dependencies
## Usage
```rust
use ev_core::{Vehicle, VehicleBuilder};
let vehicle = VehicleBuilder::new()
.make("tesla", "Tesla")
.build()?;
```
## Architecture
This crate forms the Core of the Hexagonal Architecture and has no
dependencies on I/O, HTTP, or database libraries.
12. Code Quality Tools¶
12.1. Rustfmt Configuration¶
Create .rustfmt.toml:
edition = "2024"
max_width = 100
tab_spaces = 4
newline_style = "Unix"
use_small_heuristics = "Default"
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
12.2. Clippy Configuration¶
Create .clippy.toml:
Run with strict lints:
cargo clippy --all-features -- -D warnings \
-W clippy::pedantic \
-W clippy::nursery \
-W clippy::unwrap_used \
-W clippy::expect_used
12.3. Allowed Lints (When Necessary)¶
// At crate level for specific exceptions
#![allow(clippy::module_name_repetitions)] // Vehicle in VehicleRepository
// At item level for specific cases
#[allow(clippy::too_many_arguments)] // Rare, justified cases
pub fn complex_function(/* many params */) {}
12.4. CI Quality Gates¶
All code must pass:
# Format check
cargo fmt --all -- --check
# Linting
cargo clippy --all-targets --all-features -- -D warnings
# Tests
cargo test --all-features
# Documentation
cargo doc --no-deps --all-features
# Audit
cargo audit
13. Common Patterns¶
13.1. Configuration Management¶
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Config {
pub database_url: String,
pub port: u16,
#[serde(default = "default_log_level")]
pub log_level: String,
}
fn default_log_level() -> String {
"info".to_string()
}
impl Config {
pub fn from_env() -> Result<Self> {
envy::from_env().context("Failed to load configuration from environment")
}
}
13.2. Logging¶
Use tracing for structured logging:
use tracing::{info, warn, error, debug, instrument};
#[instrument(skip(repository))]
pub async fn load_vehicle(
id: &VehicleId,
repository: &impl VehicleRepository,
) -> Result<Vehicle> {
debug!("Loading vehicle with id: {}", id);
let vehicle = repository.find_by_id(id).await?;
match vehicle {
Some(v) => {
info!("Successfully loaded vehicle: {} {}", v.make, v.model);
Ok(v)
}
None => {
warn!("Vehicle not found: {}", id);
Err(VehicleError::NotFound)
}
}
}
13.3. Extension Traits¶
// Extend external types with project-specific behavior
pub trait VehicleExt {
fn display_name(&self) -> String;
fn is_recent(&self) -> bool;
}
impl VehicleExt for Vehicle {
fn display_name(&self) -> String {
format!("{} {} {}", self.year, self.make.name, self.model.name)
}
fn is_recent(&self) -> bool {
let current_year = chrono::Utc::now().year() as u16;
self.year.0 >= current_year - 3
}
}
13.4. Type State Pattern¶
// Different states represented by types
pub struct Unvalidated;
pub struct Validated;
pub struct Vehicle<S> {
data: VehicleData,
_state: PhantomData<S>,
}
impl Vehicle<Unvalidated> {
pub fn new(data: VehicleData) -> Self {
Self { data, _state: PhantomData }
}
pub fn validate(self) -> Result<Vehicle<Validated>, ValidationError> {
// Validation logic
Ok(Vehicle { data: self.data, _state: PhantomData })
}
}
impl Vehicle<Validated> {
// Only validated vehicles can be saved
pub fn save(&self, repo: &mut impl VehicleRepository) -> Result<()> {
repo.save(&self.data)
}
}
14. Anti-Patterns to Avoid¶
14.1. Stringly-Typed Code¶
// Bad - relying on strings
fn get_vehicle_field(vehicle: &Vehicle, field: &str) -> Option<String> {
match field {
"make" => Some(vehicle.make.clone()),
"model" => Some(vehicle.model.clone()),
_ => None,
}
}
// Good - use enums
enum VehicleField {
Make,
Model,
Year,
}
fn get_vehicle_field(vehicle: &Vehicle, field: VehicleField) -> String {
match field {
VehicleField::Make => vehicle.make.clone(),
VehicleField::Model => vehicle.model.clone(),
VehicleField::Year => vehicle.year.to_string(),
}
}
14.2. Overusing clone()¶
// Bad - unnecessary clone
pub fn format_vehicles(vehicles: &[Vehicle]) -> Vec<String> {
vehicles.iter()
.map(|v| v.clone()) // Unnecessary!
.map(|v| format!("{} {}", v.make, v.model))
.collect()
}
// Good - just borrow
pub fn format_vehicles(vehicles: &[Vehicle]) -> Vec<String> {
vehicles.iter()
.map(|v| format!("{} {}", v.make, v.model))
.collect()
}
14.3. God Objects¶
// Bad - one type does everything
pub struct VehicleManager {
pub data: Vehicle,
pub db_connection: Connection,
pub http_client: Client,
pub cache: Cache,
// ... more responsibilities
}
// Good - separate concerns
pub struct Vehicle { /* data only */ }
pub struct VehicleRepository { /* persistence */ }
pub struct VehicleService { /* business logic */ }
pub struct VehicleCache { /* caching */ }
14.4. Ignoring Results¶
// Bad - silently ignoring errors
let _ = vehicle.validate(); // Error ignored!
// Good - handle or propagate
vehicle.validate()?;
// Or explicitly acknowledge
let _ = vehicle.validate()
.map_err(|e| eprintln!("Validation warning: {}", e));
14.5. Using unwrap() in Production Code¶
// Bad - can panic
let vehicle = repository.find_by_id(&id).unwrap();
// Good - handle error
let vehicle = repository.find_by_id(&id)?;
// Or provide context
let vehicle = repository.find_by_id(&id)
.expect("Vehicle must exist at this point"); // Only if truly infallible
Summary¶
Following these guidelines ensures:
- Correctness: Type safety prevents invalid states
- Maintainability: Clear separation of concerns and consistent patterns
- Performance: Efficient use of Rust's zero-cost abstractions
- Security: Proper input validation and secure practices
- Testability: Dependency injection and trait-based design
- Documentation: Self-documenting code with comprehensive docs
When in doubt, prioritize: 1. Correctness over performance 2. Clarity over cleverness 3. Simplicity over optimization
All code submitted to the project will be reviewed against these guidelines.
Last Updated: 2025-12-25