Skip to main content

nntp_proxy/types/config/
cache.rs

1//! Cache capacity configuration types
2
3use nutype::nutype;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6/// Cache capacity in bytes
7///
8/// Supports human-readable formats:
9/// - "1gb" = 1 GB
10/// - "500mb" = 500 MB  
11/// - "64mb" = 64 MB (default)
12/// - 10000 = 10,000 bytes
13#[nutype(
14    validate(greater = 0),
15    derive(
16        Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, TryFrom, Into, AsRef,
17    )
18)]
19pub struct CacheCapacity(u64);
20
21impl Default for CacheCapacity {
22    fn default() -> Self {
23        // 64 MB - safe because we know it's > 0
24        Self::try_new(64 * 1024 * 1024).expect("default capacity is valid")
25    }
26}
27
28impl CacheCapacity {
29    /// Get capacity in bytes
30    #[inline]
31    pub fn get(&self) -> u64 {
32        self.into_inner()
33    }
34
35    /// Get capacity as u64 for moka
36    #[inline]
37    pub fn as_u64(&self) -> u64 {
38        self.into_inner()
39    }
40}
41
42impl std::str::FromStr for CacheCapacity {
43    type Err = String;
44
45    fn from_str(s: &str) -> Result<Self, Self::Err> {
46        let s = s.trim().to_lowercase();
47
48        // Try to parse as bytesize (supports kb, mb, gb, etc)
49        if let Ok(size) = s.parse::<bytesize::ByteSize>() {
50            let bytes = size.as_u64();
51            return Self::try_new(bytes).map_err(|e| e.to_string());
52        }
53
54        // Fallback to plain number (bytes)
55        match s.parse::<u64>() {
56            Ok(bytes) => Self::try_new(bytes).map_err(|e| e.to_string()),
57            Err(_) => Err(format!("Invalid cache capacity: {}", s)),
58        }
59    }
60}
61
62impl std::fmt::Display for CacheCapacity {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        write!(f, "{}", bytesize::ByteSize::b(self.into_inner()))
65    }
66}
67
68impl Serialize for CacheCapacity {
69    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
70    where
71        S: Serializer,
72    {
73        self.into_inner().serialize(serializer)
74    }
75}
76
77impl<'de> Deserialize<'de> for CacheCapacity {
78    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
79    where
80        D: Deserializer<'de>,
81    {
82        #[derive(Deserialize)]
83        #[serde(untagged)]
84        enum StringOrU64 {
85            String(String),
86            U64(u64),
87        }
88
89        match StringOrU64::deserialize(deserializer)? {
90            StringOrU64::U64(bytes) => Self::try_new(bytes).map_err(serde::de::Error::custom),
91            StringOrU64::String(s) => s.parse().map_err(serde::de::Error::custom),
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_default() {
102        // 64 MB
103        assert_eq!(CacheCapacity::default().get(), 64 * 1024 * 1024);
104    }
105
106    #[test]
107    fn test_zero_rejected() {
108        assert!(CacheCapacity::try_new(0).is_err());
109    }
110
111    #[test]
112    fn test_valid_values() {
113        assert_eq!(CacheCapacity::try_new(1).unwrap().get(), 1);
114        assert_eq!(CacheCapacity::try_new(5000).unwrap().get(), 5000);
115    }
116
117    #[test]
118    fn test_from_str_bytes() {
119        assert_eq!("2000".parse::<CacheCapacity>().unwrap().get(), 2000);
120        assert!("0".parse::<CacheCapacity>().is_err());
121        assert!("not_a_number".parse::<CacheCapacity>().is_err());
122    }
123
124    #[test]
125    fn test_from_str_units() {
126        // bytesize crate uses decimal (1 GB = 1,000,000,000 bytes)
127        // not binary (1 GiB = 1,073,741,824 bytes)
128        assert_eq!("1gb".parse::<CacheCapacity>().unwrap().get(), 1_000_000_000);
129        assert_eq!("500mb".parse::<CacheCapacity>().unwrap().get(), 500_000_000);
130        assert_eq!("10kb".parse::<CacheCapacity>().unwrap().get(), 10_000);
131
132        // For binary units, use GiB/MiB/KiB
133        assert_eq!(
134            "1gib".parse::<CacheCapacity>().unwrap().get(),
135            1024 * 1024 * 1024
136        );
137    }
138
139    #[test]
140    fn test_ordering() {
141        let small = CacheCapacity::try_new(100).unwrap();
142        let large = CacheCapacity::try_new(1000).unwrap();
143        assert!(small < large);
144    }
145}