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
serdederives 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 clippypasses -
cargo testpasses