use chrono::{DateTime, Duration, ParseError, Utc};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CacheExpiryWindow {
pub cached_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
}
impl CacheExpiryWindow {
#[must_use]
pub fn from_now(ttl: Duration) -> Self {
Self::from_base(Utc::now(), ttl)
}
#[must_use]
pub fn from_base(cached_at: DateTime<Utc>, ttl: Duration) -> Self {
Self {
cached_at,
expires_at: cached_at + ttl,
}
}
#[must_use]
pub fn is_expired_at(&self, now: DateTime<Utc>) -> bool {
is_expired(self.expires_at, now)
}
#[must_use]
pub fn is_valid_at(&self, now: DateTime<Utc>) -> bool {
is_valid(self.expires_at, now)
}
#[must_use]
pub fn cached_at_rfc3339(&self) -> String {
self.cached_at.to_rfc3339()
}
#[must_use]
pub fn expires_at_rfc3339(&self) -> String {
self.expires_at.to_rfc3339()
}
}
#[must_use]
pub fn now_rfc3339() -> String {
Utc::now().to_rfc3339()
}
#[must_use]
pub fn is_expired(expires_at: DateTime<Utc>, now: DateTime<Utc>) -> bool {
expires_at <= now
}
#[must_use]
pub fn is_valid(expires_at: DateTime<Utc>, now: DateTime<Utc>) -> bool {
expires_at > now
}
pub fn parse_rfc3339_utc(raw: &str) -> Result<DateTime<Utc>, ParseError> {
DateTime::parse_from_rfc3339(raw).map(|dt| dt.with_timezone(&Utc))
}
#[cfg(test)]
mod tests {
use super::*;
fn dt(secs: i64) -> DateTime<Utc> {
DateTime::<Utc>::from_timestamp(secs, 0).expect("valid timestamp")
}
#[test]
fn window_from_base_keeps_exact_ttl_delta() {
let cached = dt(1_700_000_000);
let window = CacheExpiryWindow::from_base(cached, Duration::seconds(90));
assert_eq!(window.expires_at - window.cached_at, Duration::seconds(90));
}
#[test]
fn validity_and_expiry_follow_strict_gt_contract() {
let cached = dt(1_700_000_000);
let window = CacheExpiryWindow::from_base(cached, Duration::seconds(30));
let at_expiry = cached + Duration::seconds(30);
assert!(window.is_valid_at(cached));
assert!(!window.is_valid_at(at_expiry));
assert!(window.is_expired_at(at_expiry));
}
#[test]
fn rfc3339_round_trip_preserves_timestamp() {
let cached = dt(1_700_000_000);
let window = CacheExpiryWindow::from_base(cached, Duration::seconds(60));
let parsed_cached = parse_rfc3339_utc(&window.cached_at_rfc3339()).unwrap();
let parsed_expires = parse_rfc3339_utc(&window.expires_at_rfc3339()).unwrap();
assert_eq!(parsed_cached, window.cached_at);
assert_eq!(parsed_expires, window.expires_at);
}
}