# Agent Guidelines This document contains guidelines and best practices for AI agents working with this codebase. ## Error Management - Use `anyhow::Result` for error handling in services and repositories. - Create domain errors using `thiserror`. - Never implement `From` for converting domain errors, manually convert them ## Writing Tests - All tests should be written in three discrete steps: ```rust,ignore use pretty_assertions::assert_eq; // Always use pretty assertions fn test_foo() { let setup = ...; // Instantiate a fixture or setup for the test let actual = ...; // Execute the fixture to create an output let expected = ...; // Define a hand written expected result assert_eq!(actual, expected); // Assert that the actual result matches the expected result } ``` - Use `pretty_assertions` for better error messages. - Use fixtures to create test data. - Use `assert_eq!` for equality checks. - Use `assert!(...)` for boolean checks. - Use unwraps in test functions and anyhow::Result in fixtures. - Keep the boilerplate to a minimum. - Use words like `fixture`, `actual` and `expected` in test functions. - Fixtures should be generic and reusable. - Test should always be written in the same file as the source code. - Use `new`, Default and derive_setters::Setters to create `actual`, `expected` and specially `fixtures`. For example: **Good:** ```rust,ignore User::default().age(12).is_happy(true).name("John") User::new("Job").age(12).is_happy() User::test() // Special test constructor ``` **Bad:** ```rust,ignore User {name: "John".to_string(), is_happy: true, age: 12} User::with_name("Job") // Bad name, should stick to User::new() or User::test() ``` - Use `unwrap()` unless the error information is useful. Use `expect` instead of `panic!` when error message is useful. For example: **Good:** ```rust,ignore users.first().expect("List should not be empty") ``` **Bad:** ```rust,ignore if let Some(user) = users.first() { // ... } else { panic!("List should not be empty") } ``` - Prefer using `assert_eq` on full objects instead of asserting each field: **Good:** ```rust,ignore assert_eq!(actual, expected); ``` **Bad:** ```rust,ignore assert_eq!(actual.a, expected.a); assert_eq!(actual.b, expected.b); ``` ## Verification Always verify changes by running tests and linting the codebase 1. Run crate specific tests to ensure they pass. ``` cargo insta test --accept ``` 2. **Build Guidelines**: - **NEVER** run `cargo build --release` unless absolutely necessary (e.g., performance testing, creating binaries for distribution) - For verification, use `cargo check` (fastest), `cargo insta test`, or `cargo build` (debug mode) - Release builds take significantly longer and are rarely needed for development verification ## Writing Domain Types - Use `derive_setters` to derive setters and use the `strip_option` and the `into` attributes on the struct types. ## Documentation - **Always** write Rust docs (`///`) for all public methods, functions, structs, enums, and traits. - Document parameters with `# Arguments` and errors with `# Errors` sections when applicable. - **Do not include code examples** - docs are for LLMs, not humans. Focus on clear, concise functionality descriptions. ## Refactoring - If asked to fix failing tests, always confirm whether to update the implementation or the tests. ## Git Operations - Safely assume git is pre-installed - Safely assume github cli (gh) is pre-installed - Always use `Co-Authored-By: ForgeCode ` for git commits and Github comments ## Service Implementation Guidelines Services should follow clean architecture principles and maintain clear separation of concerns: ### Core Principles - **No service-to-service dependencies**: Services should never depend on other services directly - **Infrastructure dependency**: Services should depend only on infrastructure abstractions when needed - **Single type parameter**: Services should take at most one generic type parameter for infrastructure - **No trait objects**: Avoid `Box` - use concrete types and generics instead - **Constructor pattern**: Implement `new()` without type bounds - apply bounds only on methods that need them - **Compose dependencies**: Use the `+` operator to combine multiple infrastructure traits into a single bound - **Arc for infrastructure**: Store infrastructure as `Arc` for cheap cloning and shared ownership - **Tuple struct pattern**: For simple services with single dependency, use tuple structs `struct Service(Arc)` ### Examples #### Simple Service (No Infrastructure) ```rust,ignore pub struct UserValidationService; impl UserValidationService { pub fn new() -> Self { ... } pub fn validate_email(&self, email: &str) -> Result<()> { // Validation logic here ... } pub fn validate_age(&self, age: u32) -> Result<()> { // Age validation logic here ... } } ``` #### Service with Infrastructure Dependency ```rust,ignore // Infrastructure trait (defined in infrastructure layer) pub trait UserRepository { fn find_by_email(&self, email: &str) -> Result>; fn save(&self, user: &User) -> Result<()>; } // Service with single generic parameter using Arc pub struct UserService { repository: Arc, } impl UserService { // Constructor without type bounds, takes Arc pub fn new(repository: Arc) -> Self { ... } } impl UserService { // Business logic methods have type bounds where needed pub fn create_user(&self, email: &str, name: &str) -> Result { ... } pub fn find_user(&self, email: &str) -> Result> { ... } } ``` #### Tuple Struct Pattern for Simple Services ```rust,ignore // Infrastructure traits pub trait FileReader { async fn read_file(&self, path: &Path) -> Result; } pub trait Environment { fn max_file_size(&self) -> u64; } // Tuple struct for simple single dependency service pub struct FileService(Arc); impl FileService { // Constructor without bounds pub fn new(infra: Arc) -> Self { ... } } impl FileService { // Business logic methods with composed trait bounds pub async fn read_with_validation(&self, path: &Path) -> Result { ... } } ``` ### Anti-patterns to Avoid ```rust,ignore // BAD: Service depending on another service pub struct BadUserService { repository: R, email_service: E, // Don't do this! } // BAD: Using trait objects pub struct BadUserService { repository: Box, // Avoid Box } // BAD: Multiple infrastructure dependencies with separate type parameters pub struct BadUserService { repository: R, cache: C, logger: L, // Too many generic parameters - hard to use and test } impl BadUserService { // BAD: Constructor with type bounds makes it hard to use pub fn new(repository: R, cache: C, logger: L) -> Self { ... } } // BAD: Usage becomes cumbersome let service = BadUserService::::new(...); ```