# 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:
```mermaid
graph TD
Repo[Repository Layer
#40;forge-repositories#41;]
Trait[RedisClient Trait
#40;forge-shared#41;]
Adapter[ExtensionRedisClient
#40;adapter#41;]
Redis[Redis Module
#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` |
| `hash_get` | Get a single field value | `Result` |
| `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, 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, String>` |
| `set_del` | Remove member | `Result<(), String>` |
#### Common Operations
| Method | Description | Returns |
| ------------ | ------------------- | ---------------------- |
| `key_exists` | Check if key exists | `Result` |
| `delete_key` | Delete key | `Result<(), String>` |
### Usage Example
```rust
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:"
```rust
// 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:
```rust
// list_range returns JSON: ["item1", "item2", "item3"]
let items = client.list_range("mylist".to_string(), 0, -1)?;
// items: Vec = 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`.
```rust
// In forge_shared/src/redis_client.rs
pub trait RedisClient: Send + Sync {
fn hash_exists(&self, key: String, field: String) -> Result;
}
```
2. **Implement the Method**: Add the implementation to `ExtensionRedisClient`.
```rust
impl RedisClient for ExtensionRedisClient {
fn hash_exists(&self, key: String, field: String) -> Result {
// 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.
```rust
fn hash_exists(&self, key: String, field: String) -> Result {
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.
```rust
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>,
}
impl MockRedisClient {
pub fn new() -> Self {
Self {
data: RwLock::new(HashMap::new()),
}
}
}
```
3. **Implement the Trait**: Implement all `RedisClient` methods.
```rust
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 {
// 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`.
```rust
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`** | Read-heavy workloads, concurrent readers | Good (multiple readers) | Standard library |
| **`Mutex`** | 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