use std::collections::HashMap;
use std::time::Duration;
use chrono::{DateTime, Utc};
use super::types::entity_descriptor::EntityDescriptor;
#[derive(Debug, Clone)]
pub struct CachedMetadata {
pub metadata: EntityDescriptor,
pub fetched_at: DateTime<Utc>,
pub cache_duration: Option<Duration>,
pub valid_until: Option<DateTime<Utc>>,
}
impl CachedMetadata {
pub fn new(
metadata: EntityDescriptor,
fetched_at: DateTime<Utc>,
cache_duration: Option<Duration>,
valid_until: Option<DateTime<Utc>>,
) -> Self {
CachedMetadata {
metadata,
fetched_at,
cache_duration,
valid_until,
}
}
pub fn is_cache_stale(&self, now: DateTime<Utc>) -> bool {
if let Some(duration) = self.cache_duration {
let stale_at = self.fetched_at + duration;
now >= stale_at
} else {
false
}
}
pub fn is_valid(&self, now: DateTime<Utc>) -> bool {
if let Some(valid_until) = self.valid_until {
now < valid_until
} else {
true
}
}
pub fn should_refresh(&self, now: DateTime<Utc>) -> bool {
self.is_cache_stale(now) || !self.is_valid(now)
}
}
pub trait MetadataStore {
fn get(&self, entity_id: &str) -> Option<&CachedMetadata>;
fn put(&mut self, entity_id: String, metadata: CachedMetadata);
fn remove(&mut self, entity_id: &str) -> Option<CachedMetadata>;
fn purge_expired(&mut self, now: DateTime<Utc>);
}
#[derive(Debug, Default)]
pub struct MetadataCache {
entries: HashMap<String, CachedMetadata>,
}
impl MetadataCache {
pub fn new() -> Self {
MetadataCache {
entries: HashMap::new(),
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl MetadataStore for MetadataCache {
fn get(&self, entity_id: &str) -> Option<&CachedMetadata> {
self.entries.get(entity_id)
}
fn put(&mut self, entity_id: String, metadata: CachedMetadata) {
self.entries.insert(entity_id, metadata);
}
fn remove(&mut self, entity_id: &str) -> Option<CachedMetadata> {
self.entries.remove(entity_id)
}
fn purge_expired(&mut self, now: DateTime<Utc>) {
self.entries.retain(|_, cached| cached.is_valid(now));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::types::entity_descriptor::{EntityDescriptor, EntityRoles};
use chrono::TimeZone;
fn dummy_entity(entity_id: &str) -> EntityDescriptor {
EntityDescriptor {
entity_id: entity_id.to_string(),
id: None,
valid_until: None,
cache_duration: None,
has_signature: false,
extensions: None,
roles: EntityRoles::Roles {
idp_sso: vec![],
sp_sso: vec![],
authn_authority: vec![],
attr_authority: vec![],
pdp: vec![],
},
organization: None,
contact_persons: vec![],
additional_metadata_locations: vec![],
}
}
#[test]
fn test_cache_stale_not_invalid() {
let fetched_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let valid_until = Utc.with_ymd_and_hms(2025, 12, 31, 0, 0, 0).unwrap();
let cache_duration = Duration::from_secs(3600);
let cached = CachedMetadata::new(
dummy_entity("https://example.com"),
fetched_at,
Some(cache_duration),
Some(valid_until),
);
let now = Utc.with_ymd_and_hms(2025, 1, 1, 2, 0, 0).unwrap();
assert!(cached.is_cache_stale(now));
assert!(cached.is_valid(now));
assert!(cached.should_refresh(now));
}
#[test]
fn test_cache_not_stale_not_invalid() {
let fetched_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let valid_until = Utc.with_ymd_and_hms(2025, 12, 31, 0, 0, 0).unwrap();
let cache_duration = Duration::from_secs(3600);
let cached = CachedMetadata::new(
dummy_entity("https://example.com"),
fetched_at,
Some(cache_duration),
Some(valid_until),
);
let now = Utc.with_ymd_and_hms(2025, 1, 1, 0, 30, 0).unwrap();
assert!(!cached.is_cache_stale(now));
assert!(cached.is_valid(now));
assert!(!cached.should_refresh(now));
}
#[test]
fn test_cache_invalid() {
let fetched_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let valid_until = Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap();
let cached = CachedMetadata::new(
dummy_entity("https://example.com"),
fetched_at,
None,
Some(valid_until),
);
let now = Utc.with_ymd_and_hms(2025, 7, 1, 0, 0, 0).unwrap();
assert!(!cached.is_cache_stale(now)); assert!(!cached.is_valid(now)); }
#[test]
fn test_cache_no_expiry() {
let fetched_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let cached =
CachedMetadata::new(dummy_entity("https://example.com"), fetched_at, None, None);
let now = Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap();
assert!(!cached.is_cache_stale(now));
assert!(cached.is_valid(now));
}
#[test]
fn test_metadata_cache_basic() {
let mut cache = MetadataCache::new();
assert!(cache.is_empty());
let now = Utc::now();
cache.put(
"https://example.com".to_string(),
CachedMetadata::new(dummy_entity("https://example.com"), now, None, None),
);
assert_eq!(cache.len(), 1);
assert!(cache.get("https://example.com").is_some());
assert!(cache.get("https://other.com").is_none());
}
#[test]
fn test_metadata_cache_purge() {
let mut cache = MetadataCache::new();
let now = Utc.with_ymd_and_hms(2025, 6, 1, 0, 0, 0).unwrap();
let valid = Utc.with_ymd_and_hms(2025, 12, 31, 0, 0, 0).unwrap();
cache.put(
"valid".to_string(),
CachedMetadata::new(dummy_entity("valid"), now, None, Some(valid)),
);
let expired = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
cache.put(
"expired".to_string(),
CachedMetadata::new(dummy_entity("expired"), now, None, Some(expired)),
);
assert_eq!(cache.len(), 2);
cache.purge_expired(now);
assert_eq!(cache.len(), 1);
assert!(cache.get("valid").is_some());
assert!(cache.get("expired").is_none());
}
}