# Forge Repositories This crate provides the data access layer for the Forge application, implementing the repository pattern to abstract database operations from business logic. ## Architecture The repository layer sits between the service layer and the database, providing a clean abstraction for data persistence. ```mermaid graph TD Services[Services Layer] Repositories[Repositories Layer
#40;This Module#41;] Database[Database] Services --> Repositories Repositories --> Database ``` ### Dual Storage Strategy The implementation uses a dual storage strategy in Redis to optimize for different access patterns: - **Hash Maps (`HMSET`/`HGETALL`):** Used for entity data (Actors, Organizations) to allow O(1) access to specific fields and efficient partial updates. - **Sets (`SADD`/`SMEMBERS`):** Used for relationships (e.g., Organization Members) to ensure uniqueness and provide efficient membership testing. ## Key Features - **Redis Integration:** Efficient hash-based storage for data - **JSON Serialization:** Automatic conversion between Rust structs and Redis - **Type Safety:** Strong typing with error handling - **Performance Optimized:** Hash operations for fast field-level access - **Flexible Client:** Generic over Redis client implementations - **Atomic Operations:** Uses Redis atomicity guarantees for data integrity ## Actor Repository The `ActorRepository` handles persistence for player data. ### Storage Format Actors are stored in Redis as hash maps: ```text actor:{uid} -> Hash { "uid": "76561198123456789", "name": "PlayerName", "bank": "1500.0", ... } ``` ### Usage Example ```rust use forge_repositories::ActorRepository; use forge_models::Actor; async fn example_usage(repo: &R) -> Result<(), String> { // 1. Create let actor = Actor::new("76561198123456789".to_string())?; repo.create(&actor)?; // 2. Retrieve if let Some(retrieved) = repo.get_by_id("76561198123456789")? { println!("Found actor: {}", retrieved.name()); } // 3. Update // Updates are atomic and preserve fields not present in the update let mut actor_to_update = repo.get_by_id("76561198123456789")?.unwrap(); // ... modify actor ... repo.update(&actor_to_update)?; // 4. Check Existence if repo.exists("76561198123456789")? { println!("Actor exists"); } // 5. Delete repo.delete("76561198123456789")?; Ok(()) } ``` ## Organization Repository The `OrgRepository` handles persistence for organizations (guilds/clans) and their members. ### Storage Format - **Org Data:** `org:{org_id}` (Hash) - **Members:** `org:{org_id}:members` (Set) ### Usage Example ```rust use forge_repositories::OrgRepository; use forge_models::{Org, MemberSummary}; async fn example_usage(repo: &R) -> Result<(), String> { // 1. Create Organization let org = Org::new("elite_squad".to_string(), "leader_uid".to_string(), "Elite Squad".to_string())?; repo.create(&org)?; // 2. Manage Members // Add a member (idempotent, handles duplicates) repo.add_member("elite_squad", "member_uid_1")?; // Get all members let members = repo.get_members("elite_squad")?; for member in members { println!("Member: {} ({})", member.name, member.uid); } // Remove a member repo.remove_member("elite_squad", "member_uid_1")?; // 3. Update Organization let mut org_update = repo.get_by_id("elite_squad")?.unwrap(); // ... modify org ... repo.update(&org_update)?; // 4. Delete Organization // Note: This removes the org data but may require separate cleanup for members depending on implementation repo.delete("elite_squad")?; Ok(()) } ``` ## Performance & Implementation Details ### Atomicity - **Upserts:** `create` and `update` operations use `HMSET` which is atomic. This means either all fields are updated or none are. - **Schema Evolution:** New fields added to the Rust structs are automatically persisted to Redis. Old fields in Redis that are no longer in the struct are **preserved** (not deleted) during updates, allowing for safe backward compatibility. ### Thread Safety All repository implementations are `Send + Sync`, allowing them to be safely shared across threads. The underlying `RedisClient` handles connection pooling and concurrent access. ### Error Handling Repositories return `Result` (or custom error types) to propagate database failures, serialization errors, or validation issues up to the service layer. ## Contributing We welcome contributions to the Forge Repository Layer! This guide will help you understand how to add new repositories and maintain the existing codebase. ### Adding a New Repository To add a new repository (e.g., `ItemRepository`), follow these steps: 1. **Create the Module**: Create a new file in `src/` (e.g., `src/item.rs`). 2. **Define the Trait**: Define a trait that specifies the data access operations. Ensure it requires `Send + Sync`. ```rust pub trait ItemRepository: Send + Sync { fn create(&self, item: &Item) -> Result<(), String>; fn get_by_id(&self, id: &str) -> Result, String>; // ... other methods } ``` 3. **Implement for Redis**: Implement the trait for a generic `RedisClient`. Use `forge_shared` helpers for value conversion if needed. ```rust use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; pub struct RedisItemRepository { client: C, } impl RedisItemRepository { pub fn new(client: C) -> Self { Self { client } } } impl ItemRepository for RedisItemRepository { fn create(&self, item: &Item) -> Result<(), String> { let redis_key = format!("item:{}", item.id); // ... serialization logic ... // Use self.client to interact with Redis self.client.hash_mset(redis_key, fields) } // ... other methods } ``` 4. **Register the Module**: Add your new module to `src/lib.rs` and export the trait and implementation. ```rust pub mod item; pub use item::{ItemRepository, RedisItemRepository}; ``` ### Testing - **Integration Tests**: Write integration tests that use a real Redis instance (if available) or a mock. - **Mocking**: For unit testing services, you don't need to test the repository implementation itself, but you should ensure the repository trait is easy to mock. ### Best Practices - **Abstraction**: Keep the repository trait agnostic of the underlying database technology (e.g., don't expose Redis-specific types in the trait signature). - **Serialization**: Handle serialization/deserialization within the repository implementation. The service layer should work with domain models, not raw JSON or database rows. - **Keyspace**: Use a consistent naming convention for Redis keys (e.g., `entity:id`). - **Atomicity**: Use Redis transactions or atomic commands where possible to ensure data consistency.