262 lines
9.1 KiB
Markdown
262 lines
9.1 KiB
Markdown
# 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<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
|
|
|
|
```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<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`.
|
|
```rust
|
|
// 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`.
|
|
```rust
|
|
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.
|
|
```rust
|
|
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.
|
|
```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<HashMap<String, String>>,
|
|
}
|
|
|
|
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<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`.
|
|
```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<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
|