Jacob Schmidt d178e39164 Refactor client UI stores and normalize docs formatting
- Rework org and store UI state modules (rename/move store/getter files, add runtime and bridge wiring)
- Update store UI components and page structure (navbar/cart split, new StoreView flow)
- Apply broad markdown/YAML/HTML/CSS/JS formatting cleanup across docs, templates, and workflows
2026-03-10 19:13:30 -05:00
..
2025-11-26 18:33:09 -06:00

Adapters Module

This module provides adapter implementations that bridge the repository layer with the extension's Redis operations. Adapters translate between the generic RedisClient trait and the extension-specific Redis module.

Architecture

The adapters module follows the Adapter Pattern, allowing the repository layer to remain decoupled from the specific Redis implementation:

graph TD
    Repo[Repository Layer<br/>#40;forge-repositories#41;]
    Trait[RedisClient Trait<br/>#40;forge-shared#41;]
    Adapter[ExtensionRedisClient<br/>#40;adapter#41;]
    Redis[Redis Module<br/>#40;extension#41;]

    Repo --> Trait
    Trait --> Adapter
    Adapter --> Redis

This design enables:

  • Testability: Repositories can use mock adapters for testing
  • Flexibility: Different Redis implementations can be swapped without changing repositories
  • Separation of Concerns: Repository logic is independent of Redis connection details

ExtensionRedisClient

The ExtensionRedisClient is the primary adapter that implements the RedisClient trait from forge_shared.

Responsibilities

  • Translate Calls: Convert trait method calls to Redis module function calls
  • Error Handling: Parse Redis operation results and convert to Result types
  • Data Transformation: Handle response parsing (e.g., JSON arrays for lists/sets)
  • Logging: Log debug information for Redis operations

Implemented Operations

Hash Operations

Method Description Returns
hash_mset Set multiple fields atomically Result<(), String>
hash_get_all Get all fields and values Result<String, String>
hash_get Get a single field value Result<String, String>
hash_del Delete a field Result<(), String>

List Operations

Method Description Returns
list_rpush Append to list Result<(), String>
list_range Get range of elements Result<Vec<String>, String>
list_del Remove by value Result<(), String>

Set Operations

Method Description Returns
set_add Add member Result<(), String>
set_members Get all members Result<Vec<String>, String>
set_del Remove member Result<(), String>

Common Operations

Method Description Returns
key_exists Check if key exists Result<bool, String>
delete_key Delete key Result<(), String>

Usage Example

use crate::adapters::ExtensionRedisClient;
use forge_shared::RedisClient;

// Create the adapter
let client = ExtensionRedisClient::new();

// Use it with the RedisClient trait
let fields = vec![
    ("name".to_string(), "John".to_string()),
    ("age".to_string(), "30".to_string()),
];
client.hash_mset("user:123".to_string(), fields)?;

// Retrieve data
let data = client.hash_get_all("user:123".to_string())?;

Error Handling

The adapter translates Redis string responses to Rust Result types:

  • Success: Returns Ok(value) with the appropriate type
  • Error: Returns Err(message) if the response starts with "Error:"
// Redis module returns "OK" → Adapter returns Ok(())
// Redis module returns "Error: Connection failed" → Adapter returns Err("Error: Connection failed")

Response Parsing

For operations that return collections, the adapter parses JSON responses:

// list_range returns JSON: ["item1", "item2", "item3"]
let items = client.list_range("mylist".to_string(), 0, -1)?;
// items: Vec<String> = vec!["item1", "item2", "item3"]

Contributing

We welcome contributions to the adapters module! Follow these guidelines to add new adapter methods or create new adapters.

Adding a New Method to ExtensionRedisClient

To add a new method (e.g., hash_exists), follow these steps:

  1. Check the Trait: Ensure the method is defined in the RedisClient trait in forge_shared.

    // In forge_shared/src/redis_client.rs
    pub trait RedisClient: Send + Sync {
        fn hash_exists(&self, key: String, field: String) -> Result<bool, String>;
    }
    
  2. Implement the Method: Add the implementation to ExtensionRedisClient.

    impl RedisClient for ExtensionRedisClient {
        fn hash_exists(&self, key: String, field: String) -> Result<bool, String> {
            // Call the Redis module function
            let result = redis::hash::hash_exists(key, field);
    
            // Parse the response
            match result.as_str() {
                "1" => Ok(true),
                "0" => Ok(false),
                _ if result.starts_with("Error:") => Err(result),
                _ => Err(format!("Unexpected response: {}", result)),
            }
        }
    }
    
  3. Add Logging (if needed): For debugging, log the operation.

    fn hash_exists(&self, key: String, field: String) -> Result<bool, String> {
        let result = redis::hash::hash_exists(key, field);
        log("debug", "DEBUG", &format!("hash_exists({}, {}): {}", key, field, result));
    
        match result.as_str() {
            "1" => Ok(true),
            "0" => Ok(false),
            _ if result.starts_with("Error:") => Err(result),
            _ => Err(format!("Unexpected response: {}", result)),
        }
    }
    
  4. Handle Response Types: Match the return type to the trait signature.

    • Unit type (()): Return Ok(()) on success
    • Boolean: Parse "1"/"0" to true/false
    • String: Return the value directly
    • Vec: Parse JSON array response
    • Number: Parse string to number

Creating a New Adapter

To create a new adapter (e.g., MockRedisClient for testing):

  1. Create the Module File: Add src/adapters/mock_client.rs.

  2. Define the Struct: Create the adapter struct.

    use forge_shared::RedisClient;
    use std::collections::HashMap;
    use std::sync::RwLock;
    
    /// Mock Redis client for testing.
    ///
    /// Uses RwLock to allow multiple concurrent readers while maintaining thread safety.
    pub struct MockRedisClient {
        data: RwLock<HashMap<String, String>>,
    }
    
    impl MockRedisClient {
        pub fn new() -> Self {
            Self {
                data: RwLock::new(HashMap::new()),
            }
        }
    }
    
  3. Implement the Trait: Implement all RedisClient methods.

    impl RedisClient for MockRedisClient {
        fn hash_mset(&self, key: String, fields: Vec<(String, String)>) -> Result<(), String> {
            // Acquire write lock only when modifying data
            let mut data = self.data.write().unwrap();
            for (field, value) in fields {
                let hash_key = format!("{}:{}", key, field);
                data.insert(hash_key, value);
            }
            Ok(())
        }
    
        fn hash_get(&self, key: String, field: String) -> Result<String, String> {
            // Acquire read lock - multiple threads can read concurrently
            let data = self.data.read().unwrap();
            let hash_key = format!("{}:{}", key, field);
            Ok(data.get(&hash_key)
                .map(|v| v.clone())
                .unwrap_or_default())
        }
    
        // ... implement other methods
    }
    
  4. Register the Module: Add to src/adapters/mod.rs.

    pub mod redis_client;
    pub mod mock_client;
    
    pub use redis_client::ExtensionRedisClient;
    pub use mock_client::MockRedisClient;
    

Concurrency Best Practices

Important

Choose the right synchronization primitive based on your access patterns and performance requirements.

Recommended Synchronization Primitives:

Primitive Use Case Performance Dependency
RwLock<HashMap> Read-heavy workloads, concurrent readers Good (multiple readers) Standard library
Mutex<HashMap> Write-heavy or exclusive access required Fair (single lock) Standard library
DashMap Extreme high-frequency reads/writes Excellent (lock-free) External crate

When to use each:

  • RwLock: Best for most use cases. Allows multiple concurrent readers, only blocks on writes. Use this by default.
  • Mutex: Only when you need exclusive access or operations are very lightweight (< 1μs).
  • DashMap: When profiling shows RwLock is a bottleneck and you need lock-free performance.

Why avoid Mutex for read-heavy workloads?

  • Blocks all threads (readers and writers) on every access
  • No concurrent reads possible
  • Can cause performance bottlenecks in high-concurrency scenarios

Best Practices

  • Error Consistency: Always check for "Error:" prefix in Redis responses
  • Type Safety: Ensure return types match the trait signature exactly
  • Logging: Log operations at DEBUG level for troubleshooting
  • Response Parsing: Handle all possible response formats (success, error, unexpected)
  • Documentation: Document the purpose and behavior of each method
  • Testing: Test adapters with both success and error scenarios