use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::Plugin;
use serde::Serialize;
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum CacheValue {
String(String),
Int(i64),
Float(f64),
List(VecDeque<String>),
Set(HashSet<String>),
Hash(HashMap<String, String>),
SortedSet(BTreeMap<String, f64>),
}
struct CacheEntry {
value: CacheValue,
expires_at: Option<Instant>,
#[allow(dead_code)]
created_at: Instant,
last_accessed: Instant,
}
impl CacheEntry {
fn new(value: CacheValue, ttl: Option<u64>) -> Self {
let now = Instant::now();
Self {
value,
expires_at: ttl.map(|s| now + Duration::from_secs(s)),
created_at: now,
last_accessed: now,
}
}
fn is_expired(&self) -> bool {
self.expires_at
.map(|exp| Instant::now() >= exp)
.unwrap_or(false)
}
fn touch(&mut self) {
self.last_accessed = Instant::now();
}
}
pub struct CachePlugin {
store: Mutex<HashMap<String, CacheEntry>>,
max_keys: usize,
stats: Mutex<CacheStats>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub sets: u64,
pub deletes: u64,
pub evictions: u64,
pub expired: u64,
}
fn glob_match(pattern: &str, text: &str) -> bool {
let mut pi = 0;
let mut ti = 0;
let pb = pattern.as_bytes();
let tb = text.as_bytes();
let mut star_pi = usize::MAX;
let mut star_ti = 0;
while ti < tb.len() {
if pi < pb.len() && (pb[pi] == b'?' || pb[pi] == tb[ti]) {
pi += 1;
ti += 1;
} else if pi < pb.len() && pb[pi] == b'*' {
star_pi = pi;
star_ti = ti;
pi += 1;
} else if star_pi != usize::MAX {
pi = star_pi + 1;
star_ti += 1;
ti = star_ti;
} else {
return false;
}
}
while pi < pb.len() && pb[pi] == b'*' {
pi += 1;
}
pi == pb.len()
}
impl CachePlugin {
pub fn new(max_keys: usize) -> Self {
Self {
store: Mutex::new(HashMap::new()),
max_keys,
stats: Mutex::new(CacheStats::default()),
}
}
fn record_hit(&self) {
self.stats.lock().unwrap().hits += 1;
}
fn record_miss(&self) {
self.stats.lock().unwrap().misses += 1;
}
fn evict_lru(&self, store: &mut HashMap<String, CacheEntry>) {
if store.len() < self.max_keys {
return;
}
let victim = store
.iter()
.min_by_key(|(_, entry)| entry.last_accessed)
.map(|(k, _)| k.clone());
if let Some(key) = victim {
store.remove(&key);
self.stats.lock().unwrap().evictions += 1;
}
}
fn remove_if_expired(&self, store: &mut HashMap<String, CacheEntry>, key: &str) -> bool {
let expired = store.get(key).map(|e| e.is_expired()).unwrap_or(false);
if expired {
store.remove(key);
self.stats.lock().unwrap().expired += 1;
}
expired
}
pub fn set(&self, key: &str, value: &str, ttl: Option<u64>) {
let mut store = self.store.lock().unwrap();
self.evict_lru(&mut store);
store.insert(
key.to_string(),
CacheEntry::new(CacheValue::String(value.to_string()), ttl),
);
self.stats.lock().unwrap().sets += 1;
}
pub fn get(&self, key: &str) -> Option<String> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
self.record_miss();
return None;
}
match store.get_mut(key) {
Some(entry) => {
entry.touch();
let val = match &entry.value {
CacheValue::String(s) => Some(s.clone()),
CacheValue::Int(n) => Some(n.to_string()),
CacheValue::Float(f) => Some(f.to_string()),
_ => None,
};
if val.is_some() {
self.record_hit();
} else {
self.record_miss();
}
val
}
None => {
self.record_miss();
None
}
}
}
pub fn del(&self, key: &str) -> bool {
let mut store = self.store.lock().unwrap();
let existed = store.remove(key).is_some();
if existed {
self.stats.lock().unwrap().deletes += 1;
}
existed
}
pub fn exists(&self, key: &str) -> bool {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return false;
}
store.contains_key(key)
}
pub fn incr(&self, key: &str) -> Result<i64, String> {
self.incrby(key, 1)
}
pub fn decr(&self, key: &str) -> Result<i64, String> {
self.incrby(key, -1)
}
pub fn incrby(&self, key: &str, amount: i64) -> Result<i64, String> {
let mut store = self.store.lock().unwrap();
self.remove_if_expired(&mut store, key);
match store.get_mut(key) {
Some(entry) => {
entry.touch();
match &mut entry.value {
CacheValue::Int(n) => {
*n += amount;
Ok(*n)
}
CacheValue::String(s) => {
let n: i64 = s
.parse()
.map_err(|_| "value is not an integer".to_string())?;
let new_val = n + amount;
entry.value = CacheValue::Int(new_val);
Ok(new_val)
}
_ => Err("value is not an integer".to_string()),
}
}
None => {
self.evict_lru(&mut store);
store.insert(
key.to_string(),
CacheEntry::new(CacheValue::Int(amount), None),
);
Ok(amount)
}
}
}
pub fn setnx(&self, key: &str, value: &str, ttl: Option<u64>) -> bool {
let mut store = self.store.lock().unwrap();
self.remove_if_expired(&mut store, key);
if store.contains_key(key) {
return false;
}
self.evict_lru(&mut store);
store.insert(
key.to_string(),
CacheEntry::new(CacheValue::String(value.to_string()), ttl),
);
self.stats.lock().unwrap().sets += 1;
true
}
pub fn getset(&self, key: &str, value: &str) -> Option<String> {
let mut store = self.store.lock().unwrap();
self.remove_if_expired(&mut store, key);
let old = store.get(key).and_then(|entry| match &entry.value {
CacheValue::String(s) => Some(s.clone()),
CacheValue::Int(n) => Some(n.to_string()),
CacheValue::Float(f) => Some(f.to_string()),
_ => None,
});
self.evict_lru(&mut store);
store.insert(
key.to_string(),
CacheEntry::new(CacheValue::String(value.to_string()), None),
);
self.stats.lock().unwrap().sets += 1;
old
}
pub fn mget(&self, keys: &[&str]) -> Vec<Option<String>> {
let mut store = self.store.lock().unwrap();
keys.iter()
.map(|key| {
if self.remove_if_expired(&mut store, key) {
self.record_miss();
return None;
}
match store.get_mut(*key) {
Some(entry) => {
entry.touch();
match &entry.value {
CacheValue::String(s) => {
self.record_hit();
Some(s.clone())
}
CacheValue::Int(n) => {
self.record_hit();
Some(n.to_string())
}
CacheValue::Float(f) => {
self.record_hit();
Some(f.to_string())
}
_ => {
self.record_miss();
None
}
}
}
None => {
self.record_miss();
None
}
}
})
.collect()
}
pub fn mset(&self, pairs: &[(&str, &str)]) {
let mut store = self.store.lock().unwrap();
for (key, value) in pairs {
self.evict_lru(&mut store);
store.insert(
key.to_string(),
CacheEntry::new(CacheValue::String(value.to_string()), None),
);
self.stats.lock().unwrap().sets += 1;
}
}
pub fn ttl(&self, key: &str) -> i64 {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return -2;
}
match store.get(key) {
Some(entry) => match entry.expires_at {
Some(exp) => {
let now = Instant::now();
if exp > now {
(exp - now).as_secs() as i64
} else {
-2
}
}
None => -1,
},
None => -2,
}
}
pub fn expire(&self, key: &str, seconds: u64) -> bool {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return false;
}
match store.get_mut(key) {
Some(entry) => {
entry.expires_at = Some(Instant::now() + Duration::from_secs(seconds));
true
}
None => false,
}
}
pub fn persist(&self, key: &str) -> bool {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return false;
}
match store.get_mut(key) {
Some(entry) => {
let had_expiry = entry.expires_at.is_some();
entry.expires_at = None;
had_expiry
}
None => false,
}
}
pub fn keys(&self, pattern: &str) -> Vec<String> {
let mut store = self.store.lock().unwrap();
let expired: Vec<String> = store
.iter()
.filter(|(_, entry)| entry.is_expired())
.map(|(k, _)| k.clone())
.collect();
for k in &expired {
store.remove(k);
}
{
let mut stats = self.stats.lock().unwrap();
stats.expired += expired.len() as u64;
}
store
.keys()
.filter(|k| glob_match(pattern, k))
.cloned()
.collect()
}
pub fn lpush(&self, key: &str, value: &str) -> usize {
let mut store = self.store.lock().unwrap();
self.remove_if_expired(&mut store, key);
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::List(list) = &mut entry.value {
list.push_front(value.to_string());
list.len()
} else {
let mut list = VecDeque::new();
list.push_front(value.to_string());
let len = list.len();
entry.value = CacheValue::List(list);
len
}
}
None => {
self.evict_lru(&mut store);
let mut list = VecDeque::new();
list.push_front(value.to_string());
store.insert(
key.to_string(),
CacheEntry::new(CacheValue::List(list), None),
);
1
}
}
}
pub fn rpush(&self, key: &str, value: &str) -> usize {
let mut store = self.store.lock().unwrap();
self.remove_if_expired(&mut store, key);
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::List(list) = &mut entry.value {
list.push_back(value.to_string());
list.len()
} else {
let mut list = VecDeque::new();
list.push_back(value.to_string());
let len = list.len();
entry.value = CacheValue::List(list);
len
}
}
None => {
self.evict_lru(&mut store);
let mut list = VecDeque::new();
list.push_back(value.to_string());
store.insert(
key.to_string(),
CacheEntry::new(CacheValue::List(list), None),
);
1
}
}
}
pub fn lpop(&self, key: &str) -> Option<String> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return None;
}
let entry = store.get_mut(key)?;
entry.touch();
if let CacheValue::List(list) = &mut entry.value {
list.pop_front()
} else {
None
}
}
pub fn rpop(&self, key: &str) -> Option<String> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return None;
}
let entry = store.get_mut(key)?;
entry.touch();
if let CacheValue::List(list) = &mut entry.value {
list.pop_back()
} else {
None
}
}
pub fn lrange(&self, key: &str, start: i64, stop: i64) -> Vec<String> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return vec![];
}
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::List(list) = &entry.value {
let len = list.len() as i64;
if len == 0 {
return vec![];
}
let s = if start < 0 {
(len + start).max(0) as usize
} else {
start.min(len - 1) as usize
};
let e = if stop < 0 {
(len + stop).max(0) as usize
} else {
stop.min(len - 1) as usize
};
if s > e {
return vec![];
}
list.iter().skip(s).take(e - s + 1).cloned().collect()
} else {
vec![]
}
}
None => vec![],
}
}
pub fn llen(&self, key: &str) -> usize {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return 0;
}
match store.get(key) {
Some(entry) => {
if let CacheValue::List(list) = &entry.value {
list.len()
} else {
0
}
}
None => 0,
}
}
pub fn sadd(&self, key: &str, member: &str) -> bool {
let mut store = self.store.lock().unwrap();
self.remove_if_expired(&mut store, key);
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::Set(set) = &mut entry.value {
set.insert(member.to_string())
} else {
let mut set = HashSet::new();
set.insert(member.to_string());
entry.value = CacheValue::Set(set);
true
}
}
None => {
self.evict_lru(&mut store);
let mut set = HashSet::new();
set.insert(member.to_string());
store.insert(key.to_string(), CacheEntry::new(CacheValue::Set(set), None));
true
}
}
}
pub fn srem(&self, key: &str, member: &str) -> bool {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return false;
}
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::Set(set) = &mut entry.value {
set.remove(member)
} else {
false
}
}
None => false,
}
}
pub fn smembers(&self, key: &str) -> Vec<String> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return vec![];
}
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::Set(set) = &entry.value {
set.iter().cloned().collect()
} else {
vec![]
}
}
None => vec![],
}
}
pub fn sismember(&self, key: &str, member: &str) -> bool {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return false;
}
match store.get(key) {
Some(entry) => {
if let CacheValue::Set(set) = &entry.value {
set.contains(member)
} else {
false
}
}
None => false,
}
}
pub fn scard(&self, key: &str) -> usize {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return 0;
}
match store.get(key) {
Some(entry) => {
if let CacheValue::Set(set) = &entry.value {
set.len()
} else {
0
}
}
None => 0,
}
}
pub fn sinter(&self, key1: &str, key2: &str) -> Vec<String> {
let mut store = self.store.lock().unwrap();
self.remove_if_expired(&mut store, key1);
self.remove_if_expired(&mut store, key2);
let set1 = match store.get(key1) {
Some(entry) => match &entry.value {
CacheValue::Set(s) => s.clone(),
_ => return vec![],
},
None => return vec![],
};
let set2 = match store.get(key2) {
Some(entry) => match &entry.value {
CacheValue::Set(s) => s,
_ => return vec![],
},
None => return vec![],
};
set1.intersection(set2).cloned().collect()
}
pub fn sunion(&self, key1: &str, key2: &str) -> Vec<String> {
let mut store = self.store.lock().unwrap();
self.remove_if_expired(&mut store, key1);
self.remove_if_expired(&mut store, key2);
let set1 = match store.get(key1) {
Some(entry) => match &entry.value {
CacheValue::Set(s) => s.clone(),
_ => HashSet::new(),
},
None => HashSet::new(),
};
let set2 = match store.get(key2) {
Some(entry) => match &entry.value {
CacheValue::Set(s) => s,
_ => return set1.into_iter().collect(),
},
None => return set1.into_iter().collect(),
};
set1.union(set2).cloned().collect()
}
pub fn hset(&self, key: &str, field: &str, value: &str) {
let mut store = self.store.lock().unwrap();
self.remove_if_expired(&mut store, key);
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::Hash(hash) = &mut entry.value {
hash.insert(field.to_string(), value.to_string());
} else {
let mut hash = HashMap::new();
hash.insert(field.to_string(), value.to_string());
entry.value = CacheValue::Hash(hash);
}
}
None => {
self.evict_lru(&mut store);
let mut hash = HashMap::new();
hash.insert(field.to_string(), value.to_string());
store.insert(
key.to_string(),
CacheEntry::new(CacheValue::Hash(hash), None),
);
}
}
}
pub fn hget(&self, key: &str, field: &str) -> Option<String> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return None;
}
let entry = store.get_mut(key)?;
entry.touch();
if let CacheValue::Hash(hash) = &entry.value {
hash.get(field).cloned()
} else {
None
}
}
pub fn hdel(&self, key: &str, field: &str) -> bool {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return false;
}
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::Hash(hash) = &mut entry.value {
hash.remove(field).is_some()
} else {
false
}
}
None => false,
}
}
pub fn hgetall(&self, key: &str) -> HashMap<String, String> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return HashMap::new();
}
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::Hash(hash) = &entry.value {
hash.clone()
} else {
HashMap::new()
}
}
None => HashMap::new(),
}
}
pub fn hexists(&self, key: &str, field: &str) -> bool {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return false;
}
match store.get(key) {
Some(entry) => {
if let CacheValue::Hash(hash) = &entry.value {
hash.contains_key(field)
} else {
false
}
}
None => false,
}
}
pub fn hlen(&self, key: &str) -> usize {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return 0;
}
match store.get(key) {
Some(entry) => {
if let CacheValue::Hash(hash) = &entry.value {
hash.len()
} else {
0
}
}
None => 0,
}
}
pub fn hkeys(&self, key: &str) -> Vec<String> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return vec![];
}
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::Hash(hash) = &entry.value {
hash.keys().cloned().collect()
} else {
vec![]
}
}
None => vec![],
}
}
pub fn hincrby(&self, key: &str, field: &str, amount: i64) -> Result<i64, String> {
let mut store = self.store.lock().unwrap();
self.remove_if_expired(&mut store, key);
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::Hash(hash) = &mut entry.value {
let current: i64 = match hash.get(field) {
Some(v) => v
.parse()
.map_err(|_| "hash value is not an integer".to_string())?,
None => 0,
};
let new_val = current + amount;
hash.insert(field.to_string(), new_val.to_string());
Ok(new_val)
} else {
Err("key is not a hash".to_string())
}
}
None => {
self.evict_lru(&mut store);
let mut hash = HashMap::new();
hash.insert(field.to_string(), amount.to_string());
store.insert(
key.to_string(),
CacheEntry::new(CacheValue::Hash(hash), None),
);
Ok(amount)
}
}
}
pub fn zadd(&self, key: &str, score: f64, member: &str) {
let mut store = self.store.lock().unwrap();
self.remove_if_expired(&mut store, key);
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::SortedSet(zset) = &mut entry.value {
zset.insert(member.to_string(), score);
} else {
let mut zset = BTreeMap::new();
zset.insert(member.to_string(), score);
entry.value = CacheValue::SortedSet(zset);
}
}
None => {
self.evict_lru(&mut store);
let mut zset = BTreeMap::new();
zset.insert(member.to_string(), score);
store.insert(
key.to_string(),
CacheEntry::new(CacheValue::SortedSet(zset), None),
);
}
}
}
pub fn zrem(&self, key: &str, member: &str) -> bool {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return false;
}
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::SortedSet(zset) = &mut entry.value {
zset.remove(member).is_some()
} else {
false
}
}
None => false,
}
}
pub fn zscore(&self, key: &str, member: &str) -> Option<f64> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return None;
}
let entry = store.get_mut(key)?;
entry.touch();
if let CacheValue::SortedSet(zset) = &entry.value {
zset.get(member).copied()
} else {
None
}
}
pub fn zrank(&self, key: &str, member: &str) -> Option<usize> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return None;
}
let entry = store.get_mut(key)?;
entry.touch();
if let CacheValue::SortedSet(zset) = &entry.value {
let target_score = zset.get(member)?;
let mut members: Vec<(&String, &f64)> = zset.iter().collect();
members.sort_by(|a, b| {
a.1.partial_cmp(b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(b.0))
});
members
.iter()
.position(|(m, s)| *m == member && *s == target_score)
} else {
None
}
}
pub fn zrange(&self, key: &str, start: usize, stop: usize) -> Vec<(String, f64)> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return vec![];
}
match store.get_mut(key) {
Some(entry) => {
entry.touch();
if let CacheValue::SortedSet(zset) = &entry.value {
let mut members: Vec<(String, f64)> =
zset.iter().map(|(m, s)| (m.clone(), *s)).collect();
members.sort_by(|a, b| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.0.cmp(&b.0))
});
let end = stop.min(members.len().saturating_sub(1));
if start > end {
return vec![];
}
members[start..=end].to_vec()
} else {
vec![]
}
}
None => vec![],
}
}
pub fn zcard(&self, key: &str) -> usize {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return 0;
}
match store.get(key) {
Some(entry) => {
if let CacheValue::SortedSet(zset) = &entry.value {
zset.len()
} else {
0
}
}
None => 0,
}
}
pub fn dbsize(&self) -> usize {
let store = self.store.lock().unwrap();
store.values().filter(|e| !e.is_expired()).count()
}
pub fn flushall(&self) {
let mut store = self.store.lock().unwrap();
store.clear();
let mut stats = self.stats.lock().unwrap();
*stats = CacheStats::default();
}
pub fn info(&self) -> CacheStats {
self.stats.lock().unwrap().clone()
}
pub fn key_type(&self, key: &str) -> Option<&'static str> {
let mut store = self.store.lock().unwrap();
if self.remove_if_expired(&mut store, key) {
return None;
}
store.get(key).map(|entry| match &entry.value {
CacheValue::String(_) => "string",
CacheValue::Int(_) => "string",
CacheValue::Float(_) => "string",
CacheValue::List(_) => "list",
CacheValue::Set(_) => "set",
CacheValue::Hash(_) => "hash",
CacheValue::SortedSet(_) => "zset",
})
}
pub fn cleanup_expired(&self) -> usize {
let mut store = self.store.lock().unwrap();
let expired: Vec<String> = store
.iter()
.filter(|(_, entry)| entry.is_expired())
.map(|(k, _)| k.clone())
.collect();
let count = expired.len();
for k in &expired {
store.remove(k);
}
self.stats.lock().unwrap().expired += count as u64;
count
}
}
impl Plugin for CachePlugin {
fn name(&self) -> &str {
"cache"
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
fn cache() -> CachePlugin {
CachePlugin::new(1000)
}
#[test]
fn set_and_get() {
let c = cache();
c.set("hello", "world", None);
assert_eq!(c.get("hello"), Some("world".to_string()));
}
#[test]
fn get_missing_key_returns_none() {
let c = cache();
assert_eq!(c.get("nonexistent"), None);
}
#[test]
fn set_with_ttl_and_get_before_expiry() {
let c = cache();
c.set("k", "v", Some(10));
assert_eq!(c.get("k"), Some("v".to_string()));
}
#[test]
fn get_expired_key_returns_none() {
let c = cache();
c.set("k", "v", Some(0));
thread::sleep(Duration::from_millis(5));
assert_eq!(c.get("k"), None);
}
#[test]
fn incr_creates_key() {
let c = cache();
assert_eq!(c.incr("counter"), Ok(1));
assert_eq!(c.incr("counter"), Ok(2));
}
#[test]
fn decr_key() {
let c = cache();
c.set("x", "10", None);
assert_eq!(c.decr("x"), Ok(9));
assert_eq!(c.decr("x"), Ok(8));
}
#[test]
fn incrby_amount() {
let c = cache();
assert_eq!(c.incrby("n", 5), Ok(5));
assert_eq!(c.incrby("n", 3), Ok(8));
assert_eq!(c.incrby("n", -2), Ok(6));
}
#[test]
fn incr_non_integer_errors() {
let c = cache();
c.set("s", "not_a_number", None);
assert!(c.incr("s").is_err());
}
#[test]
fn setnx_only_sets_if_missing() {
let c = cache();
assert!(c.setnx("k", "first", None));
assert!(!c.setnx("k", "second", None));
assert_eq!(c.get("k"), Some("first".to_string()));
}
#[test]
fn getset_swaps_value() {
let c = cache();
c.set("k", "old", None);
let old = c.getset("k", "new");
assert_eq!(old, Some("old".to_string()));
assert_eq!(c.get("k"), Some("new".to_string()));
}
#[test]
fn getset_on_missing_key() {
let c = cache();
let old = c.getset("k", "val");
assert_eq!(old, None);
assert_eq!(c.get("k"), Some("val".to_string()));
}
#[test]
fn mget_and_mset() {
let c = cache();
c.mset(&[("a", "1"), ("b", "2"), ("c", "3")]);
let vals = c.mget(&["a", "b", "missing", "c"]);
assert_eq!(
vals,
vec![
Some("1".to_string()),
Some("2".to_string()),
None,
Some("3".to_string()),
]
);
}
#[test]
fn ttl_no_expiry() {
let c = cache();
c.set("k", "v", None);
assert_eq!(c.ttl("k"), -1);
}
#[test]
fn ttl_missing_key() {
let c = cache();
assert_eq!(c.ttl("nope"), -2);
}
#[test]
fn expire_and_persist() {
let c = cache();
c.set("k", "v", None);
assert!(c.expire("k", 100));
assert!(c.ttl("k") > 0);
assert!(c.persist("k"));
assert_eq!(c.ttl("k"), -1);
}
#[test]
fn expire_on_missing_key() {
let c = cache();
assert!(!c.expire("nope", 10));
}
#[test]
fn del_existing_key() {
let c = cache();
c.set("k", "v", None);
assert!(c.del("k"));
assert!(!c.exists("k"));
}
#[test]
fn del_missing_key() {
let c = cache();
assert!(!c.del("nope"));
}
#[test]
fn exists_key() {
let c = cache();
assert!(!c.exists("k"));
c.set("k", "v", None);
assert!(c.exists("k"));
}
#[test]
fn lpush_and_rpush() {
let c = cache();
c.lpush("list", "b");
c.lpush("list", "a");
c.rpush("list", "c");
assert_eq!(
c.lrange("list", 0, -1),
vec!["a".to_string(), "b".to_string(), "c".to_string()]
);
}
#[test]
fn lpop_and_rpop() {
let c = cache();
c.rpush("list", "a");
c.rpush("list", "b");
c.rpush("list", "c");
assert_eq!(c.lpop("list"), Some("a".to_string()));
assert_eq!(c.rpop("list"), Some("c".to_string()));
assert_eq!(c.llen("list"), 1);
}
#[test]
fn lrange_with_negative_indices() {
let c = cache();
for v in &["a", "b", "c", "d", "e"] {
c.rpush("list", v);
}
assert_eq!(
c.lrange("list", -2, -1),
vec!["d".to_string(), "e".to_string()]
);
}
#[test]
fn llen_empty_and_missing() {
let c = cache();
assert_eq!(c.llen("nope"), 0);
c.rpush("list", "x");
assert_eq!(c.llen("list"), 1);
}
#[test]
fn lpop_empty_list() {
let c = cache();
assert_eq!(c.lpop("nope"), None);
}
#[test]
fn sadd_and_sismember() {
let c = cache();
assert!(c.sadd("s", "a"));
assert!(!c.sadd("s", "a")); assert!(c.sismember("s", "a"));
assert!(!c.sismember("s", "b"));
}
#[test]
fn srem_member() {
let c = cache();
c.sadd("s", "a");
c.sadd("s", "b");
assert!(c.srem("s", "a"));
assert!(!c.sismember("s", "a"));
assert_eq!(c.scard("s"), 1);
}
#[test]
fn smembers_returns_all() {
let c = cache();
c.sadd("s", "x");
c.sadd("s", "y");
let mut members = c.smembers("s");
members.sort();
assert_eq!(members, vec!["x".to_string(), "y".to_string()]);
}
#[test]
fn scard_and_empty() {
let c = cache();
assert_eq!(c.scard("nope"), 0);
c.sadd("s", "a");
assert_eq!(c.scard("s"), 1);
}
#[test]
fn sinter_two_sets() {
let c = cache();
c.sadd("s1", "a");
c.sadd("s1", "b");
c.sadd("s1", "c");
c.sadd("s2", "b");
c.sadd("s2", "c");
c.sadd("s2", "d");
let mut inter = c.sinter("s1", "s2");
inter.sort();
assert_eq!(inter, vec!["b".to_string(), "c".to_string()]);
}
#[test]
fn sunion_two_sets() {
let c = cache();
c.sadd("s1", "a");
c.sadd("s1", "b");
c.sadd("s2", "b");
c.sadd("s2", "c");
let mut union = c.sunion("s1", "s2");
union.sort();
assert_eq!(
union,
vec!["a".to_string(), "b".to_string(), "c".to_string()]
);
}
#[test]
fn hset_and_hget() {
let c = cache();
c.hset("h", "name", "alice");
assert_eq!(c.hget("h", "name"), Some("alice".to_string()));
assert_eq!(c.hget("h", "missing"), None);
}
#[test]
fn hdel_field() {
let c = cache();
c.hset("h", "a", "1");
c.hset("h", "b", "2");
assert!(c.hdel("h", "a"));
assert!(!c.hexists("h", "a"));
assert_eq!(c.hlen("h"), 1);
}
#[test]
fn hgetall_returns_map() {
let c = cache();
c.hset("h", "x", "1");
c.hset("h", "y", "2");
let all = c.hgetall("h");
assert_eq!(all.len(), 2);
assert_eq!(all.get("x"), Some(&"1".to_string()));
assert_eq!(all.get("y"), Some(&"2".to_string()));
}
#[test]
fn hexists_and_hlen() {
let c = cache();
assert!(!c.hexists("h", "f"));
assert_eq!(c.hlen("h"), 0);
c.hset("h", "f", "v");
assert!(c.hexists("h", "f"));
assert_eq!(c.hlen("h"), 1);
}
#[test]
fn hkeys_returns_field_names() {
let c = cache();
c.hset("h", "a", "1");
c.hset("h", "b", "2");
let mut keys = c.hkeys("h");
keys.sort();
assert_eq!(keys, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn hincrby_creates_and_increments() {
let c = cache();
assert_eq!(c.hincrby("h", "count", 5), Ok(5));
assert_eq!(c.hincrby("h", "count", 3), Ok(8));
}
#[test]
fn hincrby_non_integer_errors() {
let c = cache();
c.hset("h", "name", "alice");
assert!(c.hincrby("h", "name", 1).is_err());
}
#[test]
fn zadd_and_zscore() {
let c = cache();
c.zadd("z", 1.5, "a");
c.zadd("z", 2.5, "b");
assert_eq!(c.zscore("z", "a"), Some(1.5));
assert_eq!(c.zscore("z", "b"), Some(2.5));
assert_eq!(c.zscore("z", "c"), None);
}
#[test]
fn zrem_member() {
let c = cache();
c.zadd("z", 1.0, "a");
c.zadd("z", 2.0, "b");
assert!(c.zrem("z", "a"));
assert!(!c.zrem("z", "a"));
assert_eq!(c.zcard("z"), 1);
}
#[test]
fn zrank_by_score() {
let c = cache();
c.zadd("z", 3.0, "c");
c.zadd("z", 1.0, "a");
c.zadd("z", 2.0, "b");
assert_eq!(c.zrank("z", "a"), Some(0));
assert_eq!(c.zrank("z", "b"), Some(1));
assert_eq!(c.zrank("z", "c"), Some(2));
}
#[test]
fn zrange_returns_ordered_slice() {
let c = cache();
c.zadd("z", 3.0, "c");
c.zadd("z", 1.0, "a");
c.zadd("z", 2.0, "b");
let range = c.zrange("z", 0, 1);
assert_eq!(range, vec![("a".to_string(), 1.0), ("b".to_string(), 2.0),]);
}
#[test]
fn zcard_empty_and_filled() {
let c = cache();
assert_eq!(c.zcard("z"), 0);
c.zadd("z", 1.0, "x");
assert_eq!(c.zcard("z"), 1);
}
#[test]
fn keys_star_pattern() {
let c = cache();
c.set("user:1", "a", None);
c.set("user:2", "b", None);
c.set("post:1", "c", None);
let mut matched = c.keys("user:*");
matched.sort();
assert_eq!(matched, vec!["user:1".to_string(), "user:2".to_string()]);
}
#[test]
fn keys_question_mark_pattern() {
let c = cache();
c.set("a1", "v", None);
c.set("a2", "v", None);
c.set("ab", "v", None);
let mut matched = c.keys("a?");
matched.sort();
assert_eq!(
matched,
vec!["a1".to_string(), "a2".to_string(), "ab".to_string()]
);
}
#[test]
fn keys_all_pattern() {
let c = cache();
c.set("x", "1", None);
c.set("y", "2", None);
assert_eq!(c.keys("*").len(), 2);
}
#[test]
fn key_type_detection() {
let c = cache();
c.set("s", "val", None);
c.rpush("l", "item");
c.sadd("set", "m");
c.hset("h", "f", "v");
c.zadd("z", 1.0, "m");
assert_eq!(c.key_type("s"), Some("string"));
assert_eq!(c.key_type("l"), Some("list"));
assert_eq!(c.key_type("set"), Some("set"));
assert_eq!(c.key_type("h"), Some("hash"));
assert_eq!(c.key_type("z"), Some("zset"));
assert_eq!(c.key_type("nope"), None);
}
#[test]
fn lru_eviction_when_over_max_keys() {
let c = CachePlugin::new(3);
c.set("a", "1", None);
c.set("b", "2", None);
c.set("c", "3", None);
c.get("a");
c.set("d", "4", None);
assert_eq!(c.dbsize(), 3);
assert!(c.exists("a")); assert!(!c.exists("b")); assert!(c.exists("c"));
assert!(c.exists("d"));
let stats = c.info();
assert!(stats.evictions >= 1);
}
#[test]
fn stats_hit_miss_tracking() {
let c = cache();
c.set("k", "v", None);
c.get("k"); c.get("k"); c.get("missing");
let stats = c.info();
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 1);
assert_eq!(stats.sets, 1);
}
#[test]
fn cleanup_expired_keys() {
let c = cache();
c.set("keep", "yes", None);
c.set("expire1", "no", Some(0));
c.set("expire2", "no", Some(0));
thread::sleep(Duration::from_millis(5));
let removed = c.cleanup_expired();
assert_eq!(removed, 2);
assert!(c.exists("keep"));
assert!(!c.exists("expire1"));
assert!(!c.exists("expire2"));
}
#[test]
fn dbsize_counts_non_expired() {
let c = cache();
c.set("a", "1", None);
c.set("b", "2", None);
c.set("c", "3", Some(0));
thread::sleep(Duration::from_millis(5));
assert_eq!(c.dbsize(), 2);
}
#[test]
fn flushall_clears_everything() {
let c = cache();
c.set("a", "1", None);
c.set("b", "2", None);
c.rpush("list", "x");
c.flushall();
assert_eq!(c.dbsize(), 0);
let stats = c.info();
assert_eq!(stats.sets, 0);
}
#[test]
fn plugin_name() {
let c = cache();
assert_eq!(Plugin::name(&c), "cache");
}
#[test]
fn glob_match_exact() {
assert!(glob_match("hello", "hello"));
assert!(!glob_match("hello", "world"));
}
#[test]
fn glob_match_star() {
assert!(glob_match("h*o", "hello"));
assert!(glob_match("*", "anything"));
assert!(glob_match("pre*", "prefix"));
assert!(glob_match("*fix", "suffix"));
}
#[test]
fn glob_match_question() {
assert!(glob_match("h?llo", "hello"));
assert!(!glob_match("h?llo", "hllo"));
}
}