stemedb/applications/aphoria/dogfood/cachewrap/src/client.rs
jml e758f2ebfb feat(aphoria): implement programmatic extractors for Option<T> semantics
Completes Task #3 of httpclient dogfooding with 100% detection rate (7/7 violations).

## New Extractors

- **OptionBoundsExtractor**: Detects Option<T> fields set to None (unbounded)
- **OptionValueExtractor**: Extracts values from Some(n) for threshold checks

Both extractors use context-aware pattern matching to understand Rust Option<T>
semantics, which declarative extractors cannot handle.

## Implementation

**Files Created**:
- applications/aphoria/src/extractors/option_bounds.rs (257 lines)
- applications/aphoria/src/extractors/option_value.rs (277 lines)
- applications/aphoria/docs/examples/extractors/programmatic-option-semantics.md

**Files Modified**:
- applications/aphoria/src/extractors/mod.rs - Added module declarations
- applications/aphoria/src/extractors/registry.rs - Registered extractors
- applications/aphoria/dogfood/httpclient/.aphoria/claims.toml - Added 4 claims
- applications/aphoria/dogfood/httpclient/TASK-1-SUMMARY.md - Task #3 completion

## Results

| Metric | Value |
|--------|-------|
| Detection Rate | 100% (7/7 violations) |
| Improvement | +29 percentage points (from 71%) |
| New Violations | 2 (max_redirects, max_retries unbounded) |
| Unit Tests | 13 (all passing) |

## Two-Claim Strategy

For each bounded Option<T> field:
1. **configured** claim - Detects None (unbounded)
2. **max_value** claim - Validates Some(n) threshold

Example:
- `max_redirects: None` → CONFLICT (not configured)
- `max_redirects: Some(20)` → CONFLICT (exceeds 10)
- `max_redirects: Some(5)` → PASS

## Enterprise Quality

✓ Proper error handling (no unwrap/expect)
✓ Comprehensive tests (6+7 unit tests)
✓ Full documentation with examples
✓ Reusable for 10+ similar patterns
✓ Screening patterns for performance

## Cachewrap Dogfood

Also includes complete cachewrap dogfood exercise:
- 10 claims for Redis cache wrapper
- Day 1-5 summaries
- Full retrospective and evaluation
- Declarative extractors for all patterns

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 06:43:10 +00:00

159 lines
5.1 KiB
Rust

//! Cache client implementation
use crate::config::CacheConfig;
use crate::error::{CacheError, Result};
use redis::aio::ConnectionManager;
use redis::{AsyncCommands, Client};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
/// Validate cache key for security (prevent injection attacks)
fn validate_key(key: &str) -> Result<()> {
// Check key length (prevent excessive memory use)
if key.is_empty() {
return Err(CacheError::ConfigError("Key cannot be empty".to_string()));
}
if key.len() > 512 {
return Err(CacheError::ConfigError(
"Key exceeds maximum length of 512 characters".to_string(),
));
}
// Check for control characters (prevent injection)
if key.chars().any(|c| c.is_control()) {
return Err(CacheError::ConfigError(
"Key contains invalid control characters".to_string(),
));
}
// Check for whitespace (common mistake)
if key.contains(char::is_whitespace) {
return Err(CacheError::ConfigError(
"Key contains whitespace characters".to_string(),
));
}
Ok(())
}
/// Cache client for Redis operations
pub struct CacheClient {
#[allow(dead_code)] // Will be used for metrics/config
config: Arc<CacheConfig>,
// ✅ FIXED VIOLATION 9: Using ConnectionManager for connection pooling
// @aphoria:claimed cache-max-connections-001
manager: ConnectionManager,
}
impl CacheClient {
/// Create a new cache client with connection pooling
pub async fn new(config: CacheConfig) -> Result<Self> {
let client = Client::open(config.url.as_str())
.map_err(|e| CacheError::ConnectionError(e.to_string()))?;
// ✅ Create ConnectionManager for connection pooling
let manager = ConnectionManager::new(client)
.await
.map_err(|e| CacheError::ConnectionError(e.to_string()))?;
Ok(Self {
config: Arc::new(config),
manager,
})
}
// ✅ FIXED VIOLATION 1: Key validation added
// @aphoria:claimed cache-key-validation-001
/// Get a value from the cache (WITH KEY VALIDATION)
pub async fn get(&self, key: &str) -> Result<Option<String>> {
// ✅ FIXED: Validate key before use
validate_key(key)?;
// ✅ FIXED VIOLATION 9: Using ConnectionManager (connection pooling)
let mut conn = self.manager.clone();
let value: Option<String> = conn.get(key).await?;
Ok(value)
}
// ✅ FIXED VIOLATION 4: TTL now required
// @aphoria:claimed cache-ttl-required-001
/// Set a value in the cache with TTL (default 5 minutes)
pub async fn set(&self, key: &str, value: &str) -> Result<()> {
self.set_with_ttl(key, value, 300).await // Default 5 minute TTL
}
/// Set a value with explicit TTL
pub async fn set_with_ttl(&self, key: &str, value: &str, ttl_seconds: u64) -> Result<()> {
// Validate key
validate_key(key)?;
// ✅ FIXED VIOLATION 9: Using ConnectionManager
let mut conn = self.manager.clone();
// ✅ Use SET EX with TTL
conn.set_ex::<_, _, ()>(key, value, ttl_seconds).await?;
Ok(())
}
// ✅ FIXED VIOLATION 1: Key validation added
/// Delete a value from the cache (WITH KEY VALIDATION)
pub async fn delete(&self, key: &str) -> Result<()> {
// ✅ FIXED: Validate key before use
validate_key(key)?;
// ✅ FIXED VIOLATION 9: Using ConnectionManager
let mut conn = self.manager.clone();
conn.del::<_, ()>(key).await?;
Ok(())
}
// ✅ FIXED VIOLATION 6: Removed synchronous blocking method
// @aphoria:claimed cache-async-blocking-001
// All cache operations are now async-only for proper async runtime integration
/// Health check - verify connection is alive
pub async fn health_check(&self) -> Result<bool> {
let mut conn = self.manager.clone();
let pong: String = redis::cmd("PING")
.query_async(&mut conn)
.await
.map_err(|e| CacheError::CommandError(e.to_string()))?;
Ok(pong == "PONG")
}
/// Get typed value (with serialization)
pub async fn get_typed<T>(&self, key: &str) -> Result<Option<T>>
where
T: for<'de> Deserialize<'de>,
{
let value = self.get(key).await?;
match value {
Some(json_str) => {
let typed_value: T = serde_json::from_str(&json_str)?;
Ok(Some(typed_value))
}
None => Ok(None),
}
}
/// Set typed value (with serialization)
pub async fn set_typed<T>(&self, key: &str, value: &T) -> Result<()>
where
T: Serialize,
{
let json_str = serde_json::to_string(value)?;
self.set(key, &json_str).await
}
}
// ✅ CORRECT VERSION (for reference, to be implemented in Day 4):
// - Validate keys: check length, control chars, special chars
// - Use connection pool (r2d2-redis or bb8-redis)
// - Always set TTL with SET_EX or SETEX command
// - Remove blocking_get() or mark it as deprecated
// - Add metrics tracking (hit_count, miss_count, latency)