use crate::cache::SharedCache;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct ApiKeyVersion {
pub version: String,
pub key_hash: String,
pub permissions: Vec<String>,
pub created_at: Instant,
pub expires_at: Option<Instant>,
pub is_active: bool,
}
impl Serialize for ApiKeyVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("ApiKeyVersion", 6)?;
state.serialize_field("version", &self.version)?;
state.serialize_field("key_hash", &self.key_hash)?;
state.serialize_field("permissions", &self.permissions)?;
let created_nanos = self.created_at.elapsed().as_nanos() as i64;
state.serialize_field("created_at", &created_nanos)?;
let expires_nanos = self.expires_at.map(|i| i.elapsed().as_nanos() as i64);
state.serialize_field("expires_at", &expires_nanos)?;
state.serialize_field("is_active", &self.is_active)?;
state.end()
}
}
impl<'de> Deserialize<'de> for ApiKeyVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper {
version: String,
key_hash: String,
permissions: Vec<String>,
created_at: i64,
expires_at: Option<i64>,
is_active: bool,
}
let helper = Helper::deserialize(deserializer)?;
Ok(Self {
version: helper.version,
key_hash: helper.key_hash,
permissions: helper.permissions,
created_at: Instant::now() - Duration::from_nanos(helper.created_at as u64),
expires_at: helper
.expires_at
.map(|n| Instant::now() - Duration::from_nanos(n as u64)),
is_active: helper.is_active,
})
}
}
impl ApiKeyVersion {
pub fn new(
version: String,
key_hash: String,
permissions: Vec<String>,
ttl: Option<Duration>,
) -> Self {
let created_at = Instant::now();
let expires_at = ttl.map(|duration| created_at + duration);
Self {
version,
key_hash,
permissions,
created_at,
expires_at,
is_active: true,
}
}
pub fn is_expired(&self) -> bool {
if let Some(expires_at) = self.expires_at {
Instant::now() > expires_at
} else {
false
}
}
pub fn deactivate(&mut self) {
self.is_active = false;
}
}
#[derive(Debug, Clone)]
pub struct ApiKeyMetadata {
pub key_id: String,
pub versions: Vec<ApiKeyVersion>,
pub active_version_index: Option<usize>,
pub created_at: Instant,
pub description: Option<String>,
}
impl Serialize for ApiKeyMetadata {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("ApiKeyMetadata", 5)?;
state.serialize_field("key_id", &self.key_id)?;
state.serialize_field("versions", &self.versions)?;
state.serialize_field("active_version_index", &self.active_version_index)?;
let created_nanos = self.created_at.elapsed().as_nanos() as i64;
state.serialize_field("created_at", &created_nanos)?;
state.serialize_field("description", &self.description)?;
state.end()
}
}
impl<'de> Deserialize<'de> for ApiKeyMetadata {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper {
key_id: String,
versions: Vec<ApiKeyVersion>,
active_version_index: Option<usize>,
created_at: i64,
description: Option<String>,
}
let helper = Helper::deserialize(deserializer)?;
Ok(Self {
key_id: helper.key_id,
versions: helper.versions,
active_version_index: helper.active_version_index,
created_at: Instant::now() - Duration::from_nanos(helper.created_at as u64),
description: helper.description,
})
}
}
impl ApiKeyMetadata {
pub fn new(key_id: String, description: Option<String>) -> Self {
Self {
key_id,
versions: Vec::new(),
active_version_index: None,
created_at: Instant::now(),
description,
}
}
pub fn add_version(&mut self, version: ApiKeyVersion) {
let is_active = version.is_active;
self.versions.push(version);
if self.active_version_index.is_none() || is_active {
self.active_version_index = Some(self.versions.len() - 1);
}
}
pub fn get_active_version(&self) -> Option<&ApiKeyVersion> {
self.active_version_index
.and_then(|idx| self.versions.get(idx))
.filter(|v| v.is_active && !v.is_expired())
}
pub fn rotate_to_version(&mut self, version_index: usize) -> Result<(), String> {
if version_index >= self.versions.len() {
return Err(format!("Version {} does not exist", version_index));
}
let _old_version = self
.active_version_index
.and_then(|idx| self.versions.get(idx))
.map(|v| v.version.clone())
.unwrap_or_else(|| "none".to_string());
if let Some(current_idx) = self.active_version_index {
if let Some(current) = self.versions.get_mut(current_idx) {
current.deactivate();
}
}
if let Some(new_version) = self.versions.get_mut(version_index) {
new_version.is_active = true;
self.active_version_index = Some(version_index);
Ok(())
} else {
Err(format!("Version {} not found", version_index))
}
}
pub fn cleanup_versions(&mut self, keep_last_n: usize) {
let active_idx = self.active_version_index;
let last_idx = self.versions.len().saturating_sub(1);
self.versions
.retain(|v| !v.is_expired() || active_idx == Some(last_idx));
if self.versions.len() > keep_last_n {
let active = self.get_active_version();
let active_version_name = active.map(|v| v.version.clone());
let len = self.versions.len();
let to_take = keep_last_n.min(len);
let remaining: Vec<_> = self.versions.drain(len - to_take..).collect();
let mut kept: Vec<_> = remaining.into_iter().rev().take(keep_last_n).collect();
kept.reverse();
self.versions = kept;
if let Some(ref version_name) = active_version_name {
self.active_version_index = self
.versions
.iter()
.position(|v| &v.version == version_name);
}
}
}
pub fn get_valid_versions(&self) -> Vec<&ApiKeyVersion> {
self.versions.iter().filter(|v| !v.is_expired()).collect()
}
}
#[derive(Debug, Clone)]
pub struct LruConfig {
pub max_entries: usize,
pub ttl: Duration,
pub eviction_threshold: f64,
}
impl Default for LruConfig {
fn default() -> Self {
Self {
max_entries: 10_000, ttl: Duration::from_secs(3600), eviction_threshold: 0.8, }
}
}
pub struct LruCacheManager {
cache: SharedCache,
access_times: Arc<std::sync::Mutex<HashMap<String, Instant>>>,
config: LruConfig,
}
impl LruCacheManager {
pub fn new(cache: SharedCache, config: LruConfig) -> Self {
Self {
cache,
access_times: Arc::new(std::sync::Mutex::new(HashMap::new())),
config,
}
}
pub fn get(&self, key: &str) -> Option<Vec<u8>> {
let value = self.cache.get(key)?;
if let Ok(mut times) = self.access_times.lock() {
times.insert(key.to_string(), Instant::now());
}
Some(value)
}
pub fn set(&self, key: &str, value: Vec<u8>) {
self.check_and_evict();
self.cache.set(key, value);
if let Ok(mut times) = self.access_times.lock() {
times.insert(key.to_string(), Instant::now());
}
}
pub fn delete(&self, key: &str) {
self.cache.delete(key);
if let Ok(mut times) = self.access_times.lock() {
times.remove(key);
}
}
fn check_and_evict(&self) {
let Ok(mut times) = self.access_times.lock() else {
return;
};
let now = Instant::now();
let config = &self.config;
let mut expired_keys: Vec<String> = Vec::new();
times.retain(|k, &mut last_accessed| {
if now.duration_since(last_accessed) > config.ttl {
expired_keys.push(k.clone());
false
} else {
true
}
});
for key in &expired_keys {
self.cache.delete(key);
}
let threshold = (config.max_entries as f64 * config.eviction_threshold) as usize;
if times.len() > threshold {
let mut entries: Vec<_> = times.iter().map(|(k, v)| (k.clone(), *v)).collect();
entries.sort_by_key(|&(_, time)| time);
let to_remove = entries.len().saturating_sub(threshold);
for (key, _) in entries.into_iter().take(to_remove) {
times.remove(&key);
self.cache.delete(&key);
}
}
}
pub fn stats(&self) -> LruStats {
let total_entries = self
.access_times
.lock()
.map(|times| times.len())
.unwrap_or(0);
LruStats {
total_entries,
max_entries: self.config.max_entries,
ttl: self.config.ttl,
}
}
}
#[derive(Debug, Clone)]
pub struct LruStats {
pub total_entries: usize,
pub max_entries: usize,
pub ttl: Duration,
}
#[derive(Debug, Clone)]
pub struct RotationConfig {
pub rotation_interval: Duration,
pub grace_period: Duration,
pub keep_versions: usize,
}
impl Default for RotationConfig {
fn default() -> Self {
Self {
rotation_interval: Duration::from_secs(86400 * 30), grace_period: Duration::from_secs(86400 * 7), keep_versions: 3,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_key_version_creation() {
let version = ApiKeyVersion::new(
"v1".to_string(),
"hash123".to_string(),
vec!["read".to_string()],
Some(Duration::from_secs(3600)),
);
assert_eq!(version.version, "v1");
assert_eq!(version.key_hash, "hash123");
assert!(version.is_active);
assert!(!version.is_expired());
}
#[test]
fn test_api_key_version_expiration() {
let version = ApiKeyVersion::new(
"v1".to_string(),
"hash123".to_string(),
vec![],
Some(Duration::from_millis(10)),
);
std::thread::sleep(Duration::from_millis(20));
assert!(version.is_expired());
}
#[test]
fn test_api_key_metadata_add_version() {
let mut metadata = ApiKeyMetadata::new("key1".to_string(), Some("Test key".to_string()));
let version1 = ApiKeyVersion::new(
"v1".to_string(),
"hash1".to_string(),
vec!["read".to_string()],
None,
);
metadata.add_version(version1);
assert_eq!(metadata.versions.len(), 1);
assert!(metadata.get_active_version().is_some());
}
#[test]
fn test_api_key_metadata_rotation() {
let mut metadata = ApiKeyMetadata::new("key1".to_string(), None);
let v1 = ApiKeyVersion::new("v1".to_string(), "hash1".to_string(), vec![], None);
let v2 = ApiKeyVersion::new("v2".to_string(), "hash2".to_string(), vec![], None);
metadata.add_version(v1);
metadata.add_version(v2);
let result = metadata.rotate_to_version(1);
assert!(result.is_ok());
let active = metadata.get_active_version().unwrap();
assert_eq!(active.version, "v2");
}
#[test]
fn test_api_key_metadata_cleanup() {
let mut metadata = ApiKeyMetadata::new("key1".to_string(), None);
for i in 1..=5 {
let version = ApiKeyVersion::new(format!("v{}", i), format!("hash{}", i), vec![], None);
metadata.add_version(version);
}
metadata.cleanup_versions(3);
assert!(metadata.versions.len() <= 3);
}
#[test]
fn test_lru_config_default() {
let config = LruConfig::default();
assert_eq!(config.max_entries, 10_000);
assert_eq!(config.ttl, Duration::from_secs(3600));
assert!((config.eviction_threshold - 0.8).abs() < f64::EPSILON);
}
#[test]
fn test_lru_eviction_threshold() {
let config = LruConfig {
max_entries: 100,
ttl: Duration::from_secs(3600),
eviction_threshold: 0.5, };
assert_eq!(config.max_entries, 100);
assert!((config.eviction_threshold - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_rotation_config_default() {
let config = RotationConfig::default();
assert_eq!(config.rotation_interval, Duration::from_secs(86400 * 30));
assert_eq!(config.grace_period, Duration::from_secs(86400 * 7));
assert_eq!(config.keep_versions, 3);
}
#[test]
fn test_lru_cache_manager_basic_creation() {
use crate::cache::DashMapCache;
let cache = DashMapCache::new();
let config = LruConfig::default();
let manager = LruCacheManager::new(Arc::new(cache), config);
let stats = manager.stats();
assert_eq!(stats.max_entries, 10_000);
assert_eq!(stats.ttl, Duration::from_secs(3600));
}
#[test]
fn test_lru_cache_manager_eviction() {
use crate::cache::DashMapCache;
let cache = DashMapCache::with_capacity(10);
let config = LruConfig {
max_entries: 5,
ttl: Duration::from_secs(3600),
eviction_threshold: 0.8,
};
let manager = LruCacheManager::new(Arc::new(cache), config);
for i in 0..5 {
manager.set(&format!("key_{}", i), vec![i as u8]);
}
let stats = manager.stats();
assert!(stats.total_entries <= 5);
}
#[test]
fn test_lru_cache_manager_access_order() {
use crate::cache::DashMapCache;
let cache = DashMapCache::new();
let config = LruConfig::default();
let manager = LruCacheManager::new(Arc::new(cache), config);
manager.set("key1", b"value1".to_vec());
manager.set("key2", b"value2".to_vec());
manager.set("key3", b"value3".to_vec());
let _ = manager.get("key1");
assert_eq!(manager.get("key1"), Some(b"value1".to_vec()));
}
#[test]
fn test_lru_cache_manager_custom_config() {
use crate::cache::DashMapCache;
let cache = DashMapCache::new();
let config = LruConfig {
max_entries: 500,
ttl: Duration::from_secs(1800),
eviction_threshold: 0.6,
};
let manager = LruCacheManager::new(Arc::new(cache), config);
let stats = manager.stats();
assert_eq!(stats.max_entries, 500);
assert_eq!(stats.ttl, Duration::from_secs(1800));
}
#[test]
fn test_lru_config_clone() {
let config = LruConfig {
max_entries: 5000,
ttl: Duration::from_secs(7200),
eviction_threshold: 0.9,
};
let cloned = config.clone();
assert_eq!(cloned.max_entries, 5000);
assert_eq!(cloned.ttl, Duration::from_secs(7200));
assert!((cloned.eviction_threshold - 0.9).abs() < f64::EPSILON);
}
#[test]
fn test_rotation_config_clone() {
let config = RotationConfig {
rotation_interval: Duration::from_secs(86400 * 60),
grace_period: Duration::from_secs(86400 * 14),
keep_versions: 5,
};
let cloned = config.clone();
assert_eq!(cloned.rotation_interval, Duration::from_secs(86400 * 60));
assert_eq!(cloned.grace_period, Duration::from_secs(86400 * 14));
assert_eq!(cloned.keep_versions, 5);
}
#[test]
fn test_api_key_version_deactivate() {
let mut version = ApiKeyVersion::new(
"v1".to_string(),
"hash123".to_string(),
vec!["read".to_string()],
None,
);
assert!(version.is_active);
version.deactivate();
assert!(!version.is_active);
}
#[test]
fn test_api_key_version_clone() {
let version = ApiKeyVersion::new(
"v1".to_string(),
"hash123".to_string(),
vec!["read".to_string(), "write".to_string()],
Some(Duration::from_secs(3600)),
);
let cloned = version.clone();
assert_eq!(cloned.version, "v1");
assert_eq!(cloned.key_hash, "hash123");
assert_eq!(cloned.permissions.len(), 2);
assert!(cloned.is_active);
}
#[test]
fn test_api_key_metadata_cleanup_versions() {
let mut metadata = ApiKeyMetadata::new("key1".to_string(), None);
for i in 1..=5 {
let version = ApiKeyVersion::new(format!("v{}", i), format!("hash{}", i), vec![], None);
metadata.add_version(version);
}
metadata.cleanup_versions(2);
assert!(metadata.versions.len() <= 2);
}
#[test]
fn test_api_key_metadata_get_active_version() {
let mut metadata = ApiKeyMetadata::new("key1".to_string(), None);
let v1 = ApiKeyVersion::new("v1".to_string(), "hash1".to_string(), vec![], None);
let v2 = ApiKeyVersion::new("v2".to_string(), "hash2".to_string(), vec![], None);
metadata.add_version(v1);
metadata.add_version(v2);
let _ = metadata.rotate_to_version(1);
let active = metadata.get_active_version();
assert!(active.is_some());
assert_eq!(active.unwrap().version, "v2");
}
#[test]
fn test_api_key_metadata_clone() {
let mut metadata = ApiKeyMetadata::new("key1".to_string(), Some("Test key".to_string()));
let version = ApiKeyVersion::new(
"v1".to_string(),
"hash1".to_string(),
vec!["read".to_string()],
None,
);
metadata.add_version(version);
let cloned = metadata.clone();
assert_eq!(cloned.key_id, "key1");
assert_eq!(cloned.description, Some("Test key".to_string()));
assert_eq!(cloned.versions.len(), 1);
}
#[test]
fn test_api_key_version_deserialize() {
let json = r#"{"version":"v1","key_hash":"hash123","permissions":["read"],"created_at":1000,"expires_at":null,"is_active":true}"#;
let version: ApiKeyVersion = serde_json::from_str(json).unwrap();
assert_eq!(version.version, "v1");
assert_eq!(version.key_hash, "hash123");
assert!(version.is_active);
assert_eq!(version.permissions, vec!["read".to_string()]);
}
#[test]
fn test_api_key_version_serialize_deserialize_roundtrip() {
let original = ApiKeyVersion::new(
"v1".to_string(),
"hash123".to_string(),
vec!["read".to_string()],
Some(Duration::from_secs(3600)),
);
let json = serde_json::to_string(&original).unwrap();
let deserialized: ApiKeyVersion = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.version, original.version);
assert_eq!(deserialized.key_hash, original.key_hash);
assert_eq!(deserialized.is_active, original.is_active);
}
#[test]
fn test_rotate_to_version_nonexistent_index() {
let mut metadata = ApiKeyMetadata::new("key1".to_string(), None);
let v1 = ApiKeyVersion::new("v1".to_string(), "hash1".to_string(), vec![], None);
metadata.add_version(v1);
let result = metadata.rotate_to_version(5);
assert!(result.is_err());
assert!(result.unwrap_err().contains("does not exist"));
}
#[test]
fn test_get_valid_versions_all_valid() {
let mut metadata = ApiKeyMetadata::new("key1".to_string(), None);
let v1 = ApiKeyVersion::new("v1".to_string(), "hash1".to_string(), vec![], None);
let v2 = ApiKeyVersion::new("v2".to_string(), "hash2".to_string(), vec![], None);
metadata.add_version(v1);
metadata.add_version(v2);
let valid = metadata.get_valid_versions();
assert_eq!(valid.len(), 2);
}
#[test]
fn test_get_valid_versions_with_expired() {
let mut metadata = ApiKeyMetadata::new("key1".to_string(), None);
let v1 = ApiKeyVersion::new(
"v1".to_string(),
"hash1".to_string(),
vec![],
Some(Duration::from_millis(10)),
);
let v2 = ApiKeyVersion::new("v2".to_string(), "hash2".to_string(), vec![], None);
metadata.add_version(v1);
metadata.add_version(v2);
std::thread::sleep(Duration::from_millis(20));
let valid = metadata.get_valid_versions();
assert_eq!(valid.len(), 1);
assert_eq!(valid[0].version, "v2");
}
#[test]
fn test_lru_cache_manager_delete() {
use crate::cache::DashMapCache;
let cache = DashMapCache::new();
let config = LruConfig::default();
let manager = LruCacheManager::new(Arc::new(cache), config);
manager.set("key1", b"value1".to_vec());
assert_eq!(manager.get("key1"), Some(b"value1".to_vec()));
manager.delete("key1");
assert_eq!(manager.get("key1"), None);
}
#[test]
fn test_lru_cache_manager_eviction_threshold_triggers() {
use crate::cache::DashMapCache;
let cache = DashMapCache::with_capacity(100);
let config = LruConfig {
max_entries: 3,
ttl: Duration::from_secs(3600),
eviction_threshold: 0.5, };
let manager = LruCacheManager::new(Arc::new(cache), config);
manager.set("key_0", vec![0]);
manager.set("key_1", vec![1]);
manager.set("key_2", vec![2]);
let stats = manager.stats();
assert!(stats.total_entries <= 3);
}
#[test]
fn test_lru_eviction_deletes_cache_values() {
use crate::cache::DashMapCache;
let cache: SharedCache = Arc::new(DashMapCache::with_capacity(100));
let config = LruConfig {
max_entries: 2,
ttl: Duration::from_secs(3600),
eviction_threshold: 0.5, };
let cache_ref = cache.clone();
let manager = LruCacheManager::new(cache, config);
manager.set("k1", b"v1".to_vec());
manager.set("k2", b"v2".to_vec());
manager.set("k3", b"v3".to_vec());
let remaining = ["k1", "k2", "k3"]
.iter()
.filter(|k| cache_ref.get(k).is_some())
.count();
assert!(
remaining < 3,
"Eviction must delete cached values, not just access metadata. \
Still present: {}",
remaining
);
}
#[test]
fn test_check_and_evict_returns_early_on_poisoned_mutex() {
use crate::cache::DashMapCache;
use std::panic::{self, AssertUnwindSafe};
let cache: SharedCache = Arc::new(DashMapCache::new());
let config = LruConfig::default();
let manager = LruCacheManager::new(cache.clone(), config);
let access_times = manager.access_times.clone();
let _ = panic::catch_unwind(AssertUnwindSafe(|| {
let _guard = access_times.lock().unwrap();
panic!("intentional panic to poison mutex");
}));
assert!(manager.access_times.lock().is_err());
manager.set("key1", b"value1".to_vec());
assert_eq!(cache.get("key1"), Some(b"value1".to_vec()));
let stats = manager.stats();
assert_eq!(stats.total_entries, 0);
}
#[test]
fn test_get_active_version_returns_none_when_active_expired() {
let mut metadata = ApiKeyMetadata::new("key1".to_string(), None);
let v1 = ApiKeyVersion::new(
"v1".to_string(),
"hash1".to_string(),
vec!["read".to_string()],
Some(Duration::from_millis(10)),
);
metadata.add_version(v1);
assert!(metadata.get_active_version().is_some());
std::thread::sleep(Duration::from_millis(20));
assert!(metadata.get_active_version().is_none());
}
#[test]
fn test_cleanup_versions_retain_removes_expired_when_active_not_last() {
let mut metadata = ApiKeyMetadata::new("key1".to_string(), None);
let v1 = ApiKeyVersion::new(
"v1".to_string(),
"hash1".to_string(),
vec![],
Some(Duration::from_millis(10)),
);
let v2 = ApiKeyVersion::new(
"v2".to_string(),
"hash2".to_string(),
vec![],
Some(Duration::from_millis(10)),
);
let v3 = ApiKeyVersion::new("v3".to_string(), "hash3".to_string(), vec![], None);
metadata.add_version(v1);
metadata.add_version(v2);
metadata.add_version(v3);
std::thread::sleep(Duration::from_millis(20));
metadata.rotate_to_version(0).unwrap();
assert_eq!(metadata.active_version_index, Some(0));
assert_eq!(metadata.versions.len(), 3);
metadata.cleanup_versions(100);
assert_eq!(metadata.versions.len(), 1);
assert_eq!(metadata.versions[0].version, "v3");
}
}