use rmcp::model::CallToolResult;
use serde_json::Value;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CacheScope {
Global,
#[default]
Request,
None,
}
impl CacheScope {
pub fn parse(s: &str) -> Self {
match s.to_lowercase().as_str() {
"global" => Self::Global,
"request" => Self::Request,
_ => Self::None,
}
}
pub fn should_cache(&self) -> bool {
matches!(self, Self::Global | Self::Request)
}
}
impl std::fmt::Display for CacheScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Global => write!(f, "global"),
Self::Request => write!(f, "request"),
Self::None => write!(f, "none"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CacheMetadata {
pub ttl_ms: u64,
pub scope: CacheScope,
}
impl CacheMetadata {
pub fn new(ttl_ms: u64, scope: CacheScope) -> Self {
Self { ttl_ms, scope }
}
pub fn ttl_duration(&self) -> Duration {
Duration::from_millis(self.ttl_ms)
}
pub fn should_cache(&self) -> bool {
self.scope.should_cache() && self.ttl_ms > 0
}
pub fn from_json(json: &Value) -> Option<Self> {
let ttl_ms = json.get("ttlMs")?.as_u64()?;
let scope = json
.get("cacheScope")
.and_then(|v| v.as_str())
.map(CacheScope::parse)
.unwrap_or_default();
Some(Self { ttl_ms, scope })
}
pub fn to_json(&self) -> Value {
serde_json::json!({
"ttlMs": self.ttl_ms,
"cacheScope": self.scope.to_string()
})
}
}
impl Default for CacheMetadata {
fn default() -> Self {
Self {
ttl_ms: 0,
scope: CacheScope::Request,
}
}
}
pub fn extract_cache_metadata(result: &CallToolResult) -> Option<CacheMetadata> {
result
.structured_content
.as_ref()
.and_then(CacheMetadata::from_json)
}
pub fn is_expired(metadata: &CacheMetadata, elapsed: Duration) -> bool {
elapsed >= metadata.ttl_duration()
}
pub fn apply_cache_semantics(result: &CallToolResult) -> Option<CacheMetadata> {
let metadata = extract_cache_metadata(result)?;
if metadata.should_cache() {
Some(metadata)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use rmcp::model::ContentBlock;
fn make_result(structured_content: Option<Value>) -> CallToolResult {
let mut result = CallToolResult::success(vec![ContentBlock::text("test".to_string())]);
result.is_error = None;
result.structured_content = structured_content;
result
}
#[test]
fn test_cache_scope_from_str() {
assert_eq!(CacheScope::parse("global"), CacheScope::Global);
assert_eq!(CacheScope::parse("request"), CacheScope::Request);
assert_eq!(CacheScope::parse("none"), CacheScope::None);
assert_eq!(CacheScope::parse("invalid"), CacheScope::None);
}
#[test]
fn test_cache_scope_should_cache() {
assert!(CacheScope::Global.should_cache());
assert!(CacheScope::Request.should_cache());
assert!(!CacheScope::None.should_cache());
}
#[test]
fn test_cache_scope_display() {
assert_eq!(format!("{}", CacheScope::Global), "global");
assert_eq!(format!("{}", CacheScope::Request), "request");
assert_eq!(format!("{}", CacheScope::None), "none");
}
#[test]
fn test_cache_metadata_new() {
let metadata = CacheMetadata::new(300000, CacheScope::Global);
assert_eq!(metadata.ttl_ms, 300000);
assert_eq!(metadata.scope, CacheScope::Global);
}
#[test]
fn test_cache_metadata_ttl_duration() {
let metadata = CacheMetadata::new(300000, CacheScope::Global);
assert_eq!(metadata.ttl_duration(), Duration::from_millis(300000));
}
#[test]
fn test_cache_metadata_should_cache_global() {
let metadata = CacheMetadata::new(300000, CacheScope::Global);
assert!(metadata.should_cache());
}
#[test]
fn test_cache_metadata_should_cache_request() {
let metadata = CacheMetadata::new(300000, CacheScope::Request);
assert!(metadata.should_cache());
}
#[test]
fn test_cache_metadata_should_not_cache_none_scope() {
let metadata = CacheMetadata::new(300000, CacheScope::None);
assert!(!metadata.should_cache());
}
#[test]
fn test_cache_metadata_should_not_cache_zero_ttl() {
let metadata = CacheMetadata::new(0, CacheScope::Global);
assert!(!metadata.should_cache());
}
#[test]
fn test_cache_metadata_from_json() {
let json = serde_json::json!({
"ttlMs": 300000,
"cacheScope": "global"
});
let metadata = CacheMetadata::from_json(&json).unwrap();
assert_eq!(metadata.ttl_ms, 300000);
assert_eq!(metadata.scope, CacheScope::Global);
}
#[test]
fn test_cache_metadata_from_json_missing_ttl() {
let json = serde_json::json!({"cacheScope": "global"});
assert!(CacheMetadata::from_json(&json).is_none());
}
#[test]
fn test_cache_metadata_from_json_default_scope() {
let json = serde_json::json!({"ttlMs": 60000});
let metadata = CacheMetadata::from_json(&json).unwrap();
assert_eq!(metadata.ttl_ms, 60000);
assert_eq!(metadata.scope, CacheScope::Request); }
#[test]
fn test_cache_metadata_to_json() {
let metadata = CacheMetadata::new(300000, CacheScope::Global);
let json = metadata.to_json();
assert_eq!(json["ttlMs"], 300000);
assert_eq!(json["cacheScope"], "global");
}
#[test]
fn test_extract_cache_metadata_present() {
let structured = serde_json::json!({"ttlMs": 300000, "cacheScope": "global"});
let result = make_result(Some(structured));
let metadata = extract_cache_metadata(&result).unwrap();
assert_eq!(metadata.ttl_ms, 300000);
assert_eq!(metadata.scope, CacheScope::Global);
}
#[test]
fn test_extract_cache_metadata_absent() {
let result = make_result(None);
assert!(extract_cache_metadata(&result).is_none());
}
#[test]
fn test_is_expired_not_expired() {
let metadata = CacheMetadata::new(300000, CacheScope::Global);
assert!(!is_expired(&metadata, Duration::from_millis(100000)));
}
#[test]
fn test_is_expired_expired() {
let metadata = CacheMetadata::new(300000, CacheScope::Global);
assert!(is_expired(&metadata, Duration::from_millis(300000)));
}
#[test]
fn test_is_expired_exceeded() {
let metadata = CacheMetadata::new(300000, CacheScope::Global);
assert!(is_expired(&metadata, Duration::from_millis(400000)));
}
#[test]
fn test_apply_cache_semantics_global() {
let structured = serde_json::json!({"ttlMs": 300000, "cacheScope": "global"});
let result = make_result(Some(structured));
let metadata = apply_cache_semantics(&result).unwrap();
assert_eq!(metadata.scope, CacheScope::Global);
assert!(metadata.should_cache());
}
#[test]
fn test_apply_cache_semantics_request() {
let structured = serde_json::json!({"ttlMs": 300000, "cacheScope": "request"});
let result = make_result(Some(structured));
let metadata = apply_cache_semantics(&result).unwrap();
assert_eq!(metadata.scope, CacheScope::Request);
}
#[test]
fn test_apply_cache_semantics_none_scope() {
let structured = serde_json::json!({"ttlMs": 300000, "cacheScope": "none"});
let result = make_result(Some(structured));
assert!(apply_cache_semantics(&result).is_none());
}
#[test]
fn test_apply_cache_semantics_zero_ttl() {
let structured = serde_json::json!({"ttlMs": 0, "cacheScope": "global"});
let result = make_result(Some(structured));
assert!(apply_cache_semantics(&result).is_none());
}
#[test]
fn test_apply_cache_semantics_no_metadata() {
let result = make_result(None);
assert!(apply_cache_semantics(&result).is_none());
}
#[test]
fn test_cache_metadata_default() {
let metadata = CacheMetadata::default();
assert_eq!(metadata.ttl_ms, 0);
assert_eq!(metadata.scope, CacheScope::Request);
}
#[test]
fn test_cache_metadata_equality() {
let a = CacheMetadata::new(300000, CacheScope::Global);
let b = CacheMetadata::new(300000, CacheScope::Global);
assert_eq!(a, b);
}
#[test]
fn test_cache_metadata_clone() {
let metadata = CacheMetadata::new(300000, CacheScope::Global);
let cloned = metadata.clone();
assert_eq!(metadata, cloned);
}
#[test]
fn test_cache_metadata_debug() {
let metadata = CacheMetadata::new(300000, CacheScope::Global);
let debug_str = format!("{:?}", metadata);
assert!(debug_str.contains("300000"));
assert!(debug_str.contains("Global"));
}
}