use std::fmt; use std::str::FromStr; use std::time::{Duration, SystemTime}; use serde::{Deserialize, Deserializer}; /// How long data must have gone unused before `clean` will delete it. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct MinAge(Duration); impl MinAge { pub const fn new(threshold: Duration) -> Self { Self(threshold) } pub const fn as_duration(self) -> Duration { self.0 } /// Whether data last used at `last_used` has gone untouched long enough to delete. /// /// Data with a `last_used` in the future is never eligible: that means the /// clock moved backwards, and guessing at a real age from a broken reading /// is how you delete something that is actually in use. pub fn is_eligible(self, last_used: SystemTime, now: SystemTime) -> bool { match now.duration_since(last_used) { Ok(age) => age >= self.0, Err(_) => false, } } } impl FromStr for MinAge { type Err = humantime::DurationError; fn from_str(s: &str) -> Result { humantime::parse_duration(s).map(MinAge) } } impl fmt::Display for MinAge { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", humantime::format_duration(self.0)) } } impl<'de> Deserialize<'de> for MinAge { fn deserialize>(deserializer: D) -> Result { let raw = String::deserialize(deserializer)?; raw.parse().map_err(serde::de::Error::custom) } } #[cfg(test)] mod tests { use super::*; const DAY: Duration = Duration::from_secs(60 * 60 * 24); #[test] fn parses_humantime_units() { assert_eq!("3d".parse::().unwrap(), MinAge::new(3 * DAY)); assert_eq!( "90s".parse::().unwrap(), MinAge::new(Duration::from_secs(90)) ); assert_eq!( "1h 30m".parse::().unwrap(), MinAge::new(Duration::from_secs(5400)) ); } #[test] fn rejects_nonsense() { assert!("soon".parse::().is_err()); assert!("".parse::().is_err()); } #[test] fn eligibility_is_inclusive_at_the_threshold() { let now = SystemTime::UNIX_EPOCH + 10 * DAY; let three_days = MinAge::new(3 * DAY); assert!(three_days.is_eligible(now - 3 * DAY, now)); assert!(three_days.is_eligible(now - 4 * DAY, now)); assert!(!three_days.is_eligible(now - 2 * DAY, now)); } #[test] fn zero_threshold_takes_everything_present_or_past() { let now = SystemTime::UNIX_EPOCH + 10 * DAY; assert!(MinAge::new(Duration::ZERO).is_eligible(now, now)); assert!(MinAge::new(Duration::ZERO).is_eligible(now - DAY, now)); } #[test] fn future_timestamps_are_never_eligible() { let now = SystemTime::UNIX_EPOCH + 10 * DAY; assert!(!MinAge::new(Duration::ZERO).is_eligible(now + DAY, now)); assert!(!MinAge::new(3 * DAY).is_eligible(now + 100 * DAY, now)); } }