2025-11-26 18:33:09 -06:00

162 lines
6.2 KiB
Rust

//! Bank repository implementation for data persistence operations.
//!
//! This module provides the data access layer for bank account management,
//! implementing the repository pattern to abstract database operations.
//!
//! For full documentation and examples, see the [crate README](../README.md).
use forge_models::Bank;
use forge_shared::{RedisClient, parse_json_value, parse_redis_value};
/// Repository trait defining the contract for bank data operations.
///
/// This trait abstracts the data persistence layer, allowing different
/// implementations (Redis, PostgreSQL, etc.) while maintaining a consistent
/// interface for the service layer. All implementations must be thread-safe.
pub trait BankRepository: Send + Sync {
/// Creates a new bank in the repository.
fn create(&self, bank: &Bank) -> Result<(), String>;
/// Retrieves an bank by their unique identifier.
fn get_by_id(&self, id: &str) -> Result<Option<Bank>, String>;
/// Updates an existing bank with new data.
fn update(&self, bank: &Bank) -> Result<(), String>;
/// Permanently removes an bank from the repository.
fn delete(&self, id: &str) -> Result<(), String>;
/// Checks if an bank exists in the repository.
fn exists(&self, id: &str) -> Result<bool, String>;
}
/// Redis-based implementation of the BankRepository trait.
///
/// This implementation uses Redis hash maps to store bank data, providing
/// efficient field-level access and atomic operations. Each bank is stored
/// as a separate hash with the key format `bank:{uid}`.
pub struct RedisBankRepository<C: RedisClient> {
/// The Redis client used for all database operations.
///
/// This client handles the actual communication with Redis, including
/// connection management, command execution, and error handling.
client: C,
}
impl<C: RedisClient> RedisBankRepository<C> {
/// Creates a new Redis bank repository with the provided client.
pub fn new(client: C) -> Self {
Self { client }
}
}
impl<C: RedisClient> BankRepository for RedisBankRepository<C> {
/// Creates a new bank in Redis using hash map storage.
///
/// Stores each bank as a Redis hash map with the key format `bank:{uid}`.
/// Each field of the bank struct becomes a field in the Redis hash.
fn create(&self, bank: &Bank) -> Result<(), String> {
// Generate Redis key using bank UID
let redis_key = format!("bank:{}", bank.uid());
// Serialize bank to JSON string
let bank_json =
serde_json::to_string(bank).map_err(|e| format!("Failed to serialize bank: {}", e))?;
// Parse JSON string back to Value for field extraction
let json_value: serde_json::Value = serde_json::from_str(&bank_json)
.map_err(|e| format!("Failed to parse bank JSON: {}", e))?;
// Extract fields from JSON object
if let serde_json::Value::Object(bank_map) = json_value {
// Convert each field to Redis-compatible format
let fields: Vec<(String, String)> = bank_map
.into_iter()
.map(|(field, value)| (field, parse_json_value(&value)))
.collect();
// Store all fields atomically using Redis HMSET
self.client.hash_mset(redis_key, fields)
} else {
Err("Failed to convert bank to object".to_string())
}
}
/// Retrieves an bank from Redis by their unique identifier.
///
/// Uses Redis HGETALL to retrieve all fields of the bank hash map,
/// then reconstructs the Bank struct through JSON deserialization.
fn get_by_id(&self, id: &str) -> Result<Option<Bank>, String> {
// Generate Redis key using bank UID
let redis_key = format!("bank:{}", id);
// Retrieve all hash fields from Redis
let bank_string = self.client.hash_get_all(redis_key)?;
// Return None if no data found (bank doesn't exist)
if bank_string.is_empty() {
return Ok(None);
}
// Parse comma-separated field-value pairs
let parts: Vec<&str> = bank_string.split(", ").collect();
let mut json_map = serde_json::Map::new();
let mut i = 0;
// Process pairs of field names and values
while i + 1 < parts.len() {
let key = parts[i];
let value = parts[i + 1];
// Convert Redis string value back to proper JSON type
let json_value = parse_redis_value(value);
json_map.insert(key.to_string(), json_value);
i += 2; // Move to next field-value pair
}
// Reconstruct Bank from JSON object
let json_obj = serde_json::Value::Object(json_map);
match serde_json::from_value::<Bank>(json_obj) {
Ok(bank) => Ok(Some(bank)),
// Return None for any deserialization errors (corrupted data)
Err(_) => Ok(None),
}
}
/// Updates an existing bank with the provided data.
///
/// Uses `HMSET` to atomically update the provided fields. Fields present in Redis
/// but missing from the input are preserved.
fn update(&self, bank: &Bank) -> Result<(), String> {
// Delegate to create() which handles both creation and updates
// Redis HMSET naturally supports upsert behavior
self.create(bank)
}
/// Permanently deletes an bank and all associated data from Redis.
///
/// Removes the entire Redis hash containing the bank's data.
/// This operation is irreversible.
fn delete(&self, id: &str) -> Result<(), String> {
// Generate Redis key using bank UID
let redis_key = format!("bank:{}", id);
// Delete the entire hash key from Redis
// This removes all fields and the key itself atomically
self.client.delete_key(redis_key)
}
/// Checks if an bank exists in Redis without retrieving the data.
///
/// Uses Redis EXISTS command for a lightweight check.
fn exists(&self, id: &str) -> Result<bool, String> {
// Generate Redis key using bank UID
let redis_key = format!("bank:{}", id);
// Check if the key exists in Redis
// This is a lightweight operation that doesn't retrieve data
self.client.key_exists(redis_key)
}
}