Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Adding a Feature

Follow this 7-step pattern when adding a new feature to Efision. Each step corresponds to a layer in the clean architecture.

Step-by-Step

1. Entity

Create data model structs in crates/core/src/domain/entities/{domain}/.

#![allow(unused)]
fn main() {
// domain/entities/products/my_product.rs
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct MyProduct {
    #[serde(rename = "pID")]
    pub id: i32,
    #[serde(rename = "pNO")]
    pub part_number: String,
    // ...
}
}
  • Use serde derives with #[serde(rename = "...")] for DB column mapping
  • Preserve DB column abbreviations in field names where appropriate

2. DTO (if needed)

Create DTOs in crates/core/src/domain/dtos/ for API boundaries or CLI output formatting.

3. Repository Trait

Define an async trait in crates/core/src/domain/repositories/{domain}/.

#![allow(unused)]
fn main() {
// domain/repositories/products/my_product_repo.rs
use async_trait::async_trait;
use anyhow::Result;

#[async_trait]
pub trait MyProductRepository {
    async fn count(&mut self) -> Result<i32>;
    async fn find_all(&mut self) -> Result<Vec<MyProduct>>;
    // ...
}
}

4. SQL Server Implementation

Implement the trait in crates/core/src/infrastructure/sqlserver/{domain}/.

#![allow(unused)]
fn main() {
// infrastructure/sqlserver/products/my_product_repo.rs
use async_trait::async_trait;

pub struct SqlServerMyProductRepo {
    client: Client<Compat<TcpStream>>,
}

#[async_trait]
impl MyProductRepository for SqlServerMyProductRepo {
    async fn count(&mut self) -> Result<i32> {
        let row = self.client
            .query("SELECT COUNT(*) AS total FROM MyTable WHERE status = 1", &[])
            .await?
            .into_row()
            .await?
            .context("No row returned")?;
        Ok(row.get("total").context("Missing total column")?)
    }
}
}
  • Always use parameterized queries
  • Map Tiberius errors to meaningful domain errors

5. Service

Create business logic in crates/core/src/services/{domain}/.

#![allow(unused)]
fn main() {
// services/products/my_product_service.rs
pub struct MyProductService<R: MyProductRepository> {
    repo: R,
}

impl<R: MyProductRepository> MyProductService<R> {
    pub fn new(repo: R) -> Self {
        Self { repo }
    }

    pub async fn count(&mut self) -> Result<i32> {
        self.repo.count().await
    }
}
}

6. CLI Command

Wire up the command in crates/cli/src/commands/ using clap derive macros.

#![allow(unused)]
fn main() {
#[derive(Subcommand)]
pub enum MyProductCommands {
    /// Count products
    Count,
    /// Export products to CSV
    Export {
        #[arg(short, long, default_value = "export.csv")]
        output: String,
    },
}
}

7. Route in main.rs

Add the match arm in crates/cli/src/main.rs to connect the command to the service.

Checklist

  • Entity struct with serde derives
  • Repository trait with async methods
  • SQL Server implementation with parameterized queries
  • Service with generic <R: Repository> pattern
  • CLI command with clap derives
  • Route in main.rs
  • cargo clippy passes
  • cargo test passes