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>
159 lines
5.1 KiB
Rust
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)
|