use crate::errors::{ConfigError, ConfigResult};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CacheTTL {
Minutes(u32),
Hours(u32),
Seconds(u32),
}
impl CacheTTL {
pub fn to_duration(&self) -> Duration {
match self {
CacheTTL::Seconds(s) => Duration::from_secs(*s as u64),
CacheTTL::Minutes(m) => Duration::from_secs(*m as u64 * 60),
CacheTTL::Hours(h) => Duration::from_secs(*h as u64 * 3600),
}
}
pub fn from_duration(duration: Duration) -> Self {
let total_seconds = duration.as_secs();
if total_seconds.is_multiple_of(3600) && total_seconds >= 3600 {
CacheTTL::Hours((total_seconds / 3600) as u32)
} else if total_seconds.is_multiple_of(60) && total_seconds >= 60 {
CacheTTL::Minutes((total_seconds / 60) as u32)
} else {
CacheTTL::Seconds(total_seconds as u32)
}
}
pub fn from_string(s: &str) -> ConfigResult<Self> {
if s.is_empty() {
return Err(ConfigError::InvalidValue {
field: "cache_ttl".to_string(),
value: s.to_string(),
});
}
let (number_part, unit_part) = if let Some(pos) = s.chars().position(|c| c.is_alphabetic())
{
(&s[..pos], &s[pos..])
} else {
return Err(ConfigError::InvalidDurationFormat {
format: s.to_string(),
});
};
let number: u32 = number_part
.parse()
.map_err(|_| ConfigError::InvalidDurationFormat {
format: s.to_string(),
})?;
match unit_part.to_lowercase().as_str() {
"s" | "sec" | "second" | "seconds" => Ok(CacheTTL::Seconds(number)),
"m" | "min" | "minute" | "minutes" => Ok(CacheTTL::Minutes(number)),
"h" | "hr" | "hour" | "hours" => Ok(CacheTTL::Hours(number)),
_ => Err(ConfigError::InvalidDurationFormat {
format: s.to_string(),
}),
}
}
pub fn short() -> Self {
CacheTTL::Minutes(5)
}
pub fn long() -> Self {
CacheTTL::Hours(1)
}
pub fn from_long_cache_flag(use_long_cache: bool) -> Self {
if use_long_cache {
Self::long()
} else {
Self::short()
}
}
pub fn is_long(&self) -> bool {
self.to_duration() >= Duration::from_secs(3600)
}
pub fn is_short(&self) -> bool {
!self.is_long()
}
}
impl std::fmt::Display for CacheTTL {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CacheTTL::Seconds(s) => write!(f, "{}s", s),
CacheTTL::Minutes(m) => write!(f, "{}m", m),
CacheTTL::Hours(h) => write!(f, "{}h", h),
}
}
}
impl Default for CacheTTL {
fn default() -> Self {
CacheTTL::Minutes(5)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CacheConfig {
pub ttl: CacheTTL,
pub cache_type: CacheType,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum CacheType {
#[default]
Ephemeral,
Persistent,
}
impl CacheType {
pub fn to_string(&self) -> &'static str {
match self {
CacheType::Ephemeral => "ephemeral",
CacheType::Persistent => "persistent",
}
}
}
impl CacheConfig {
pub fn new(ttl: CacheTTL, cache_type: CacheType) -> Self {
Self { ttl, cache_type }
}
pub fn ephemeral(ttl: CacheTTL) -> Self {
Self::new(ttl, CacheType::Ephemeral)
}
pub fn persistent(ttl: CacheTTL) -> Self {
Self::new(ttl, CacheType::Persistent)
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"type": self.cache_type.to_string(),
"ttl": self.ttl.to_string()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_ttl_string_conversion() {
assert_eq!(CacheTTL::Minutes(5).to_string(), "5m");
assert_eq!(CacheTTL::Hours(1).to_string(), "1h");
assert_eq!(CacheTTL::Seconds(30).to_string(), "30s");
}
#[test]
fn test_cache_ttl_duration_conversion() {
let ttl = CacheTTL::Minutes(5);
assert_eq!(ttl.to_duration(), Duration::from_secs(300));
let ttl = CacheTTL::Hours(2);
assert_eq!(ttl.to_duration(), Duration::from_secs(7200));
}
#[test]
fn test_cache_ttl_from_duration() {
let duration = Duration::from_secs(3600); assert_eq!(CacheTTL::from_duration(duration), CacheTTL::Hours(1));
let duration = Duration::from_secs(300); assert_eq!(CacheTTL::from_duration(duration), CacheTTL::Minutes(5));
let duration = Duration::from_secs(45); assert_eq!(CacheTTL::from_duration(duration), CacheTTL::Seconds(45));
}
#[test]
fn test_cache_ttl_from_string() {
assert_eq!(CacheTTL::from_string("5m").unwrap(), CacheTTL::Minutes(5));
assert_eq!(CacheTTL::from_string("1h").unwrap(), CacheTTL::Hours(1));
assert_eq!(CacheTTL::from_string("30s").unwrap(), CacheTTL::Seconds(30));
assert_eq!(CacheTTL::from_string("5M").unwrap(), CacheTTL::Minutes(5));
assert_eq!(CacheTTL::from_string("1H").unwrap(), CacheTTL::Hours(1));
assert_eq!(
CacheTTL::from_string("5minutes").unwrap(),
CacheTTL::Minutes(5)
);
assert_eq!(CacheTTL::from_string("1hour").unwrap(), CacheTTL::Hours(1));
}
#[test]
fn test_cache_ttl_from_string_errors() {
assert!(CacheTTL::from_string("").is_err());
assert!(CacheTTL::from_string("5").is_err());
assert!(CacheTTL::from_string("5x").is_err());
assert!(CacheTTL::from_string("abc").is_err());
}
#[test]
fn test_cache_ttl_predicates() {
assert!(CacheTTL::Hours(1).is_long());
assert!(CacheTTL::Hours(2).is_long());
assert!(CacheTTL::Minutes(59).is_short());
assert!(CacheTTL::Minutes(5).is_short());
}
#[test]
fn test_cache_config_json() {
let config = CacheConfig::ephemeral(CacheTTL::Minutes(5));
let json = config.to_json();
assert_eq!(json["type"], "ephemeral");
assert_eq!(json["ttl"], "5m");
}
#[test]
fn test_cache_ttl_from_flag() {
assert_eq!(CacheTTL::from_long_cache_flag(true), CacheTTL::Hours(1));
assert_eq!(CacheTTL::from_long_cache_flag(false), CacheTTL::Minutes(5));
}
}