use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CacheTtl {
Minute,
Short,
Medium,
Hour,
Day,
Week,
Custom(u64),
}
impl CacheTtl {
#[must_use]
pub fn custom_secs(secs: u64) -> Self {
Self::Custom(secs)
}
#[must_use]
pub fn to_duration(self) -> Duration {
match self {
CacheTtl::Minute => Duration::from_secs(60),
CacheTtl::Short => Duration::from_secs(5 * 60),
CacheTtl::Medium => Duration::from_secs(15 * 60),
CacheTtl::Hour => Duration::from_secs(60 * 60),
CacheTtl::Day => Duration::from_secs(24 * 60 * 60),
CacheTtl::Week => Duration::from_secs(7 * 24 * 60 * 60),
CacheTtl::Custom(secs) => Duration::from_secs(secs),
}
}
#[must_use]
pub fn as_secs(self) -> u64 {
self.to_duration().as_secs()
}
}
impl From<Duration> for CacheTtl {
fn from(d: Duration) -> Self {
let secs = d.as_secs();
match secs {
0..=90 => CacheTtl::Minute, 91..=450 => CacheTtl::Short, 451..=2700 => CacheTtl::Medium, 2701..=5400 => CacheTtl::Hour, 5401..=129600 => CacheTtl::Day, 129601..=864000 => CacheTtl::Week, _ => CacheTtl::Custom(secs), }
}
}
#[derive(Debug, Clone)]
pub struct SubmitOptions {
pub redis: bool,
pub redis_ttl: Option<CacheTtl>,
pub sql: bool,
pub state: Option<String>,
}
impl Default for SubmitOptions {
fn default() -> Self {
Self {
redis: true,
redis_ttl: None,
sql: true,
state: None,
}
}
}
impl SubmitOptions {
#[must_use]
pub fn cache(ttl: CacheTtl) -> Self {
Self {
redis: true,
redis_ttl: Some(ttl),
sql: false,
state: None,
}
}
#[must_use]
pub fn durable() -> Self {
Self {
redis: false,
redis_ttl: None,
sql: true,
state: None,
}
}
#[must_use]
pub fn with_state(mut self, state: impl Into<String>) -> Self {
self.state = Some(state.into());
self
}
#[must_use]
pub fn stores_anywhere(&self) -> bool {
self.redis || self.sql
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OptionsKey {
pub redis: bool,
pub redis_ttl: Option<CacheTtl>,
pub sql: bool,
}
impl From<&SubmitOptions> for OptionsKey {
fn from(opts: &SubmitOptions) -> Self {
Self {
redis: opts.redis,
redis_ttl: opts.redis_ttl,
sql: opts.sql,
}
}
}
impl From<SubmitOptions> for OptionsKey {
fn from(opts: SubmitOptions) -> Self {
Self::from(&opts)
}
}
impl OptionsKey {
#[must_use]
pub fn to_options(&self) -> SubmitOptions {
SubmitOptions {
redis: self.redis,
redis_ttl: self.redis_ttl,
sql: self.sql,
state: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_options() {
let opts = SubmitOptions::default();
assert!(opts.redis);
assert!(opts.redis_ttl.is_none());
assert!(opts.sql);
}
#[test]
fn test_cache_options() {
let opts = SubmitOptions::cache(CacheTtl::Hour);
assert!(opts.redis);
assert_eq!(opts.redis_ttl, Some(CacheTtl::Hour));
assert!(!opts.sql);
}
#[test]
fn test_durable_options() {
let opts = SubmitOptions::durable();
assert!(!opts.redis);
assert!(opts.sql);
}
#[test]
fn test_stores_anywhere() {
assert!(SubmitOptions::default().stores_anywhere());
assert!(SubmitOptions::cache(CacheTtl::Minute).stores_anywhere());
assert!(SubmitOptions::durable().stores_anywhere());
let nowhere = SubmitOptions {
redis: false,
sql: false,
..Default::default()
};
assert!(!nowhere.stores_anywhere());
}
#[test]
fn test_options_key_grouping() {
let opts1 = SubmitOptions::default();
let opts2 = SubmitOptions::default();
let key1 = OptionsKey::from(&opts1);
let key2 = OptionsKey::from(&opts2);
assert_eq!(key1, key2);
}
#[test]
fn test_options_key_same_ttl_enum() {
let opts1 = SubmitOptions::cache(CacheTtl::Hour);
let opts2 = SubmitOptions::cache(CacheTtl::Hour);
let key1 = OptionsKey::from(&opts1);
let key2 = OptionsKey::from(&opts2);
assert_eq!(key1, key2);
}
#[test]
fn test_options_key_different_ttl_enum() {
let opts1 = SubmitOptions::cache(CacheTtl::Hour);
let opts2 = SubmitOptions::cache(CacheTtl::Day);
let key1 = OptionsKey::from(&opts1);
let key2 = OptionsKey::from(&opts2);
assert_ne!(key1, key2);
}
#[test]
fn test_cache_ttl_to_duration() {
assert_eq!(CacheTtl::Minute.to_duration(), Duration::from_secs(60));
assert_eq!(CacheTtl::Short.to_duration(), Duration::from_secs(300));
assert_eq!(CacheTtl::Hour.to_duration(), Duration::from_secs(3600));
assert_eq!(CacheTtl::Day.to_duration(), Duration::from_secs(86400));
assert_eq!(CacheTtl::Custom(123).to_duration(), Duration::from_secs(123));
}
#[test]
fn test_cache_ttl_from_duration_snapping() {
assert_eq!(CacheTtl::from(Duration::from_secs(45)), CacheTtl::Minute);
assert_eq!(CacheTtl::from(Duration::from_secs(90)), CacheTtl::Minute);
assert_eq!(CacheTtl::from(Duration::from_secs(180)), CacheTtl::Short);
assert_eq!(CacheTtl::from(Duration::from_secs(3600)), CacheTtl::Hour);
}
#[test]
fn test_options_key_roundtrip() {
let original = SubmitOptions::cache(CacheTtl::Hour);
let key = OptionsKey::from(&original);
let recovered = key.to_options();
assert_eq!(original.redis, recovered.redis);
assert_eq!(original.redis_ttl, recovered.redis_ttl);
assert_eq!(original.sql, recovered.sql);
}
#[test]
fn test_options_key_hashable() {
use std::collections::HashMap;
let mut map: HashMap<OptionsKey, Vec<String>> = HashMap::new();
let key = OptionsKey::from(&SubmitOptions::default());
map.entry(key).or_default().push("item1".into());
assert_eq!(map.len(), 1);
}
#[test]
fn test_state_default_none() {
let opts = SubmitOptions::default();
assert!(opts.state.is_none());
}
#[test]
fn test_state_with_state_builder() {
let opts = SubmitOptions::default().with_state("delta");
assert_eq!(opts.state, Some("delta".to_string()));
}
#[test]
fn test_state_cache_with_state() {
let opts = SubmitOptions::cache(CacheTtl::Hour).with_state("pending");
assert!(opts.redis);
assert!(!opts.sql);
assert_eq!(opts.state, Some("pending".to_string()));
}
#[test]
fn test_state_durable_with_state() {
let opts = SubmitOptions::durable().with_state("archived");
assert!(!opts.redis);
assert!(opts.sql);
assert_eq!(opts.state, Some("archived".to_string()));
}
#[test]
fn test_state_to_options_preserves_none() {
let opts = SubmitOptions::cache(CacheTtl::Hour);
let key = OptionsKey::from(&opts);
let recovered = key.to_options();
assert!(recovered.state.is_none());
}
}