198 lines
7.0 KiB
Markdown
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.
|