forge/lib/repositories/README.md
2025-11-26 18:33:09 -06:00

198 lines
7.0 KiB
Markdown

# 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<br/>#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<R: ActorRepository>(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<R: OrgRepository>(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<T, String>` (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<Option<Item>, 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<C: RedisClient> {
client: C,
}
impl<C: RedisClient> RedisItemRepository<C> {
pub fn new(client: C) -> Self {
Self { client }
}
}
impl<C: RedisClient> ItemRepository for RedisItemRepository<C> {
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.