#![deny(missing_docs)]
use std::collections::HashMap;
use std::time::{Duration, Instant};
use serde_json::Value;
mod sha2;
use sha2::SimpleSha256;
pub fn make_key(tool_name: &str, args: &Value) -> String {
let mut buf = String::new();
canonicalize_into(args, &mut buf);
let mut hasher = SimpleSha256::new();
hasher.update(tool_name.as_bytes());
hasher.update(b"\0");
hasher.update(buf.as_bytes());
hex_lower(&hasher.finalize())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
pub expirations: u64,
}
impl CacheStats {
fn zero() -> Self {
Self {
hits: 0,
misses: 0,
evictions: 0,
expirations: 0,
}
}
}
type ClockFn = Box<dyn Fn() -> Instant + Send + Sync + 'static>;
struct Entry<V> {
value: V,
expires_at: Option<Instant>,
}
pub struct ToolCache<V> {
max_size: usize,
default_ttl: Option<Duration>,
clock: ClockFn,
data: HashMap<String, Entry<V>>,
order: Vec<String>,
stats: CacheStats,
}
impl<V> Default for ToolCache<V> {
fn default() -> Self {
Self::new()
}
}
impl<V> ToolCache<V> {
pub fn new() -> Self {
Self {
max_size: 1024,
default_ttl: None,
clock: Box::new(Instant::now),
data: HashMap::new(),
order: Vec::new(),
stats: CacheStats::zero(),
}
}
pub fn with_capacity(mut self, max_size: usize) -> Self {
self.max_size = max_size;
self
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.default_ttl = Some(ttl);
self
}
pub fn with_clock<F>(mut self, clock: F) -> Self
where
F: Fn() -> Instant + Send + Sync + 'static,
{
self.clock = Box::new(clock);
self
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn hits(&self) -> u64 {
self.stats.hits
}
pub fn misses(&self) -> u64 {
self.stats.misses
}
pub fn evictions(&self) -> u64 {
self.stats.evictions
}
pub fn expirations(&self) -> u64 {
self.stats.expirations
}
pub fn stats(&self) -> CacheStats {
self.stats
}
pub fn clear(&mut self) {
self.data.clear();
self.order.clear();
self.stats = CacheStats::zero();
}
pub fn get(&mut self, tool_name: &str, args: &Value) -> Option<&V> {
let key = make_key(tool_name, args);
if self.touch(&key) {
self.stats.hits += 1;
self.data.get(&key).map(|e| &e.value)
} else {
self.stats.misses += 1;
None
}
}
pub fn set(&mut self, tool_name: &str, args: &Value, value: V) {
self.insert(make_key(tool_name, args), value, self.default_ttl);
}
pub fn set_with_ttl(&mut self, tool_name: &str, args: &Value, value: V, ttl: Duration) {
self.insert(make_key(tool_name, args), value, Some(ttl));
}
pub fn get_or_set<F>(&mut self, tool_name: &str, args: &Value, compute: F) -> &V
where
F: FnOnce() -> V,
{
self.get_or_set_with_ttl_opt(tool_name, args, compute, None)
}
pub fn get_or_set_with_ttl<F>(
&mut self,
tool_name: &str,
args: &Value,
compute: F,
ttl: Duration,
) -> &V
where
F: FnOnce() -> V,
{
self.get_or_set_with_ttl_opt(tool_name, args, compute, Some(ttl))
}
fn get_or_set_with_ttl_opt<F>(
&mut self,
tool_name: &str,
args: &Value,
compute: F,
ttl: Option<Duration>,
) -> &V
where
F: FnOnce() -> V,
{
let key = make_key(tool_name, args);
if self.touch(&key) {
self.stats.hits += 1;
return &self
.data
.get(&key)
.expect("touch returned true so key must be present")
.value;
}
self.stats.misses += 1;
let value = compute();
let effective_ttl = ttl.or(self.default_ttl);
self.insert(key.clone(), value, effective_ttl);
&self
.data
.get(&key)
.expect("just inserted this key, must be present")
.value
}
pub fn invalidate(&mut self, tool_name: &str, args: &Value) -> bool {
let key = make_key(tool_name, args);
if self.data.remove(&key).is_some() {
self.remove_from_order(&key);
true
} else {
false
}
}
fn insert(&mut self, key: String, value: V, ttl: Option<Duration>) {
let expires_at = ttl.map(|d| (self.clock)() + d);
if self.data.contains_key(&key) {
self.remove_from_order(&key);
}
self.data.insert(key.clone(), Entry { value, expires_at });
self.order.push(key);
self.evict_if_needed();
}
fn touch(&mut self, key: &str) -> bool {
let Some(entry) = self.data.get(key) else {
return false;
};
if let Some(expires_at) = entry.expires_at {
if (self.clock)() >= expires_at {
self.data.remove(key);
self.remove_from_order(key);
self.stats.expirations += 1;
return false;
}
}
self.remove_from_order(key);
self.order.push(key.to_string());
true
}
fn remove_from_order(&mut self, key: &str) {
if let Some(pos) = self.order.iter().position(|k| k == key) {
self.order.remove(pos);
}
}
fn evict_if_needed(&mut self) {
if self.max_size == 0 {
return;
}
while self.data.len() > self.max_size {
if self.order.is_empty() {
break;
}
let oldest = self.order.remove(0);
if self.data.remove(&oldest).is_some() {
self.stats.evictions += 1;
}
}
}
}
pub fn cached_call<V, F>(cache: &mut ToolCache<V>, tool_name: &str, args: &Value, compute: F) -> V
where
V: Clone,
F: FnOnce() -> V,
{
cache.get_or_set(tool_name, args, compute).clone()
}
fn canonicalize_into(value: &Value, out: &mut String) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(true) => out.push_str("true"),
Value::Bool(false) => out.push_str("false"),
Value::Number(n) => out.push_str(&n.to_string()),
Value::String(s) => {
let escaped = serde_json::to_string(s).expect("string serialization cannot fail");
out.push_str(&escaped);
}
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
canonicalize_into(item, out);
}
out.push(']');
}
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
out.push('{');
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
let escaped =
serde_json::to_string(k.as_str()).expect("string serialization cannot fail");
out.push_str(&escaped);
out.push(':');
canonicalize_into(&map[*k], out);
}
out.push('}');
}
}
}
fn hex_lower(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(bytes.len() * 2);
for &b in bytes {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0x0f) as usize] as char);
}
s
}