9.1 KiB
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
Resulttypes - 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:
-
Check the Trait: Ensure the method is defined in the
RedisClienttrait inforge_shared.// In forge_shared/src/redis_client.rs pub trait RedisClient: Send + Sync { fn hash_exists(&self, key: String, field: String) -> Result<bool, String>; } -
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)), } } } -
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)), } } -
Handle Response Types: Match the return type to the trait signature.
- Unit type (
()): ReturnOk(())on success - Boolean: Parse "1"/"0" to
true/false - String: Return the value directly
- Vec: Parse JSON array response
- Number: Parse string to number
- Unit type (
Creating a New Adapter
To create a new adapter (e.g., MockRedisClient for testing):
-
Create the Module File: Add
src/adapters/mock_client.rs. -
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()), } } } -
Implement the Trait: Implement all
RedisClientmethods.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 } -
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 showsRwLockis 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