//! Organization repository implementation for data persistence operations. //! //! This module provides the data access layer for organization (guild/clan) management, //! implementing the repository pattern to abstract database operations. //! //! For full documentation and examples, see the [crate README](../README.md). use forge_models::{MemberSummary, Org}; use forge_shared::{RedisClient, parse_json_value, parse_redis_value}; /// Repository trait defining the contract for organization 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 OrgRepository: Send + Sync { /// Creates a new organization in the repository. fn create(&self, org: &Org) -> Result<(), String>; /// Retrieves an organization by its unique identifier. fn get_by_id(&self, id: &str) -> Result, String>; /// Updates an existing organization with new data. fn update(&self, org: &Org) -> Result<(), String>; /// Permanently removes an organization from the repository. fn delete(&self, id: &str) -> Result<(), String>; /// Checks if an organization exists in the repository. fn exists(&self, id: &str) -> Result; /// Adds a new member UID to an organization. fn add_member(&self, org_id: &str, member_uid: &str) -> Result<(), String>; /// Retrieves all members of an organization as a list of MemberSummary objects. fn get_members(&self, org_id: &str) -> Result, String>; /// Removes a specific member from an organization. fn remove_member(&self, org_id: &str, member_uid: &str) -> Result<(), String>; } /// Redis-based implementation of the OrgRepository trait. /// /// Uses Redis hash maps for organization data providing /// efficient field-level access and atomic operations. Each organization is stored /// as a seperate hash with the key format `org:{org_id}`. /// Member lists are stored as sets with the key format `org:{org_id}:members`. pub struct RedisOrgRepository { /// The Redis client used for all database operations. /// /// This client handles the actual communication with Redis, including /// connection management, command execution, and error handling for /// both organization and member data operations. client: C, } impl RedisOrgRepository { /// Creates a new Redis organization repository with the provided client. pub fn new(client: C) -> Self { Self { client } } } impl OrgRepository for RedisOrgRepository { /// Creates a new organization in Redis using hash map storage. /// /// Stores each organization as a Redis hash map with the key format `{org_id}:org`. /// Each field of the organization struct becomes a field in the Redis hash. fn create(&self, org: &Org) -> Result<(), String> { // Generate Redis key using organization ID let redis_key = format!("org:{}", org.id()); // Serialize organization to JSON string let org_json = serde_json::to_string(org).map_err(|e| format!("Failed to serialize org: {}", e))?; // Parse JSON string back to Value for field extraction let json_value: serde_json::Value = serde_json::from_str(&org_json) .map_err(|e| format!("Failed to parse org JSON: {}", e))?; // Extract fields from JSON object if let serde_json::Value::Object(org_map) = json_value { // Convert each field to Redis-compatible format let fields: Vec<(String, String)> = org_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 org to object".to_string()) } } /// Retrieves an organization from Redis by its unique identifier. /// /// Uses Redis HGETALL to retrieve all fields of the organization hash map, /// then reconstructs the Org struct through JSON deserialization. fn get_by_id(&self, id: &str) -> Result, String> { // Generate Redis key using organization ID let redis_key = format!("org:{}", id); // Retrieve all hash fields from Redis let org_string = self.client.hash_get_all(redis_key)?; // Return None if no data found (organization doesn't exist) if org_string.is_empty() { return Ok(None); } // Parse comma-separated field-value pairs let parts: Vec<&str> = org_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 Org from JSON object let json_obj = serde_json::Value::Object(json_map); match serde_json::from_value::(json_obj) { Ok(org) => Ok(Some(org)), // Return None for any deserialization errors (corrupted data) Err(_) => Ok(None), } } /// Updates an existing organization 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, org: &Org) -> Result<(), String> { // Delegate to create() which handles both creation and updates // Redis HMSET naturally supports upsert behavior self.create(org) } /// Permanently deletes an organization and all associated data from Redis. /// /// Removes the organization hash and related subordinate keys. /// This operation is irreversible. fn delete(&self, id: &str) -> Result<(), String> { let redis_keys = [ format!("org:{}", id), format!("org:{}:members", id), format!("org:{}:assets", id), format!("org:{}:fleet", id), ]; for redis_key in redis_keys { self.client.delete_key(redis_key)?; } Ok(()) } /// Checks if an organization exists in Redis without retrieving the data. /// /// Uses Redis EXISTS command for a lightweight check. fn exists(&self, id: &str) -> Result { // Generate Redis key using organization ID let redis_key = format!("org:{}", id); // Check if the key exists in Redis // This is a lightweight operation that doesn't retrieve data self.client.key_exists(redis_key) } /// Adds a new member to the organization. /// /// Stores member data in a Redis list associated with the organization. /// Validates that the organization exists before adding the member. fn add_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { // Check if organization exists if !self.exists(org_id)? { return Err(format!("Organization {} does not exist", org_id)); } // Generate Redis key for organization member set let redis_key = format!("org:{}:members", org_id); // Add member UID to set using SADD self.client.set_add(redis_key, member_uid.to_string()) } /// Retrieves all members of the organization. /// /// Uses Redis SMEMBERS to get all member UIDs, then retrieves member details. /// Returns a list of `MemberSummary` objects. fn get_members(&self, org_id: &str) -> Result, String> { // Generate Redis key for organization member set let redis_key = format!("org:{}:members", org_id); // Retrieve all member UIDs from the set; fall back to empty on error let uids: Vec = match self.client.set_members(redis_key) { Ok(v) => v, Err(_) => Vec::new(), }; // Pre-allocate result vector let mut result: Vec = Vec::with_capacity(uids.len()); for uid in uids { if uid.trim().is_empty() { continue; } // Lookup actor name by UID; fall back to "Unknown" on error/missing let actor_key = format!("actor:{}", uid); let raw_name = match self.client.hash_get(actor_key, "name".to_string()) { Ok(n) => n, _ => String::new(), }; let name = match parse_redis_value(&raw_name) { serde_json::Value::String(s) => s, serde_json::Value::Number(n) => n.to_string(), serde_json::Value::Bool(b) => b.to_string(), _ => "Unknown".to_string(), }; let name = if name.trim().is_empty() { "Unknown".to_string() } else { name }; result.push(MemberSummary { uid, name }); } Ok(result) } /// Removes a specific member UID from an organization. /// /// Uses Redis SREM to remove the UID from the organization's member set. fn remove_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { // Generate Redis key for organization member set let redis_key = format!("org:{}:members", org_id); // Remove the UID from the set using SREM self.client.set_del(redis_key, member_uid.to_string()) } }