Skip to main content

nntp_proxy/types/config/
limits.rs

1//! Connection and error limit configuration types
2
3use nutype::nutype;
4use std::num::NonZeroUsize;
5
6/// A non-zero maximum connections limit
7///
8/// Ensures connection pools always have at least 1 connection allowed.
9#[nutype(
10    validate(greater = 0),
11    derive(
12        Debug,
13        Clone,
14        Copy,
15        PartialEq,
16        Eq,
17        Hash,
18        Display,
19        TryFrom,
20        AsRef,
21        Deref,
22        Serialize,
23        Deserialize
24    )
25)]
26#[doc(alias = "pool_size")]
27#[doc(alias = "connection_limit")]
28pub struct MaxConnections(usize);
29
30impl MaxConnections {
31    /// Default maximum connections per backend
32    pub const DEFAULT: usize = 10;
33
34    /// Get the inner value
35    #[inline]
36    pub fn get(&self) -> usize {
37        self.into_inner()
38    }
39}
40
41/// A non-zero maximum errors threshold
42///
43/// Used to specify the maximum number of errors allowed before taking action.
44#[nutype(
45    validate(greater = 0),
46    derive(
47        Debug,
48        Clone,
49        Copy,
50        PartialEq,
51        Eq,
52        Hash,
53        Display,
54        TryFrom,
55        AsRef,
56        Deref,
57        Serialize,
58        Deserialize
59    )
60)]
61pub struct MaxErrors(u32);
62
63impl MaxErrors {
64    /// Default maximum errors threshold
65    pub const DEFAULT: u32 = 3;
66
67    /// Get the inner value
68    #[inline]
69    pub fn get(&self) -> u32 {
70        self.into_inner()
71    }
72}
73
74/// A non-zero thread count
75///
76/// Ensures thread pools always have at least 1 thread.
77/// Special handling: passing 0 to `new()` returns the number of CPU cores.
78#[repr(transparent)]
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80pub struct ThreadCount(NonZeroUsize);
81
82impl ThreadCount {
83    /// Default thread count (single-threaded)
84    pub const DEFAULT: Self = Self(NonZeroUsize::new(1).unwrap());
85
86    /// Create a new ThreadCount
87    ///
88    /// - If value is 0, returns the number of CPU cores
89    /// - Otherwise returns the specified value
90    /// - Always returns Some (never None)
91    #[must_use]
92    pub const fn new(value: usize) -> Option<Self> {
93        if value == 0 {
94            // 0 means auto-detect CPU count (handled at runtime)
95            // We can't call num_cpus() in const context, so return None
96            // and the caller should use from_value() instead
97            None
98        } else {
99            match NonZeroUsize::new(value) {
100                Some(nz) => Some(Self(nz)),
101                None => None,
102            }
103        }
104    }
105
106    /// Create a new ThreadCount from a value
107    ///
108    /// - If value is 0, returns the number of CPU cores
109    /// - Otherwise returns the specified value
110    /// - Always returns Some (never None)
111    #[must_use]
112    pub fn from_value(value: usize) -> Option<Self> {
113        if value == 0 {
114            Some(Self::num_cpus())
115        } else {
116            NonZeroUsize::new(value).map(Self)
117        }
118    }
119
120    /// Get the inner value
121    #[must_use]
122    #[inline]
123    pub const fn get(&self) -> usize {
124        self.0.get()
125    }
126
127    /// Get the number of available CPU cores (private helper)
128    fn num_cpus() -> Self {
129        let count = std::thread::available_parallelism()
130            .map(|p| p.get())
131            .unwrap_or(1);
132        // SAFETY: available_parallelism always returns at least 1
133        Self(NonZeroUsize::new(count).unwrap())
134    }
135}
136
137impl Default for ThreadCount {
138    /// Default to single-threaded (1 thread)
139    fn default() -> Self {
140        Self(NonZeroUsize::new(1).unwrap())
141    }
142}
143
144impl std::fmt::Display for ThreadCount {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        write!(f, "{}", self.get())
147    }
148}
149
150impl From<ThreadCount> for usize {
151    fn from(val: ThreadCount) -> Self {
152        val.get()
153    }
154}
155
156impl serde::Serialize for ThreadCount {
157    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
158    where
159        S: serde::Serializer,
160    {
161        serializer.serialize_u64(self.get() as _)
162    }
163}
164
165impl<'de> serde::Deserialize<'de> for ThreadCount {
166    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167    where
168        D: serde::Deserializer<'de>,
169    {
170        let value = usize::deserialize(deserializer)?;
171        Self::from_value(value).ok_or_else(|| {
172            serde::de::Error::custom("ThreadCount must be a positive integer (0 means auto-detect)")
173        })
174    }
175}
176
177impl std::str::FromStr for ThreadCount {
178    type Err = std::num::ParseIntError;
179
180    fn from_str(s: &str) -> Result<Self, Self::Err> {
181        let value = s.parse::<usize>()?;
182        Ok(Self::from_value(value).unwrap_or_else(Self::default))
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use proptest::prelude::*;
190
191    // ============================================================================
192    // Property Tests - MaxConnections
193    // ============================================================================
194
195    proptest! {
196        /// Property: Any non-zero usize round-trips through MaxConnections
197        #[test]
198        fn prop_max_connections_valid_range(value in 1usize..=10000) {
199            let max = MaxConnections::try_new(value).unwrap();
200            prop_assert_eq!(max.get(), value);
201        }
202
203        /// Property: Display shows exact value
204        #[test]
205        fn prop_max_connections_display(value in 1usize..=10000) {
206            let max = MaxConnections::try_new(value).unwrap();
207            prop_assert_eq!(max.to_string(), value.to_string());
208        }
209
210        /// Property: JSON serialization round-trips correctly
211        #[test]
212        fn prop_max_connections_serde_json(value in 1usize..=10000) {
213            let max = MaxConnections::try_new(value).unwrap();
214            let json = serde_json::to_string(&max).unwrap();
215            let parsed: MaxConnections = serde_json::from_str(&json).unwrap();
216            prop_assert_eq!(parsed.get(), value);
217        }
218
219        /// Property: Clone creates identical copy
220        #[test]
221        fn prop_max_connections_clone(value in 1usize..=10000) {
222            let max = MaxConnections::try_new(value).unwrap();
223            let cloned = max;
224            prop_assert_eq!(max, cloned);
225        }
226    }
227
228    // Edge Cases - MaxConnections
229    #[test]
230    fn test_max_connections_zero_rejected() {
231        assert!(MaxConnections::try_new(0).is_err());
232    }
233
234    #[test]
235    fn test_max_connections_default() {
236        assert_eq!(MaxConnections::DEFAULT, 10);
237    }
238
239    #[test]
240    fn test_max_connections_serde_json_zero_rejected() {
241        assert!(serde_json::from_str::<MaxConnections>("0").is_err());
242    }
243
244    // ============================================================================
245    // Property Tests - MaxErrors
246    // ============================================================================
247
248    proptest! {
249        /// Property: Any non-zero u32 round-trips through MaxErrors
250        #[test]
251        fn prop_max_errors_valid_range(value in 1u32..=1000) {
252            let max = MaxErrors::try_new(value).unwrap();
253            prop_assert_eq!(max.get(), value);
254        }
255
256        /// Property: Display shows exact value
257        #[test]
258        fn prop_max_errors_display(value in 1u32..=1000) {
259            let max = MaxErrors::try_new(value).unwrap();
260            prop_assert_eq!(max.to_string(), value.to_string());
261        }
262
263        /// Property: JSON serialization round-trips correctly
264        #[test]
265        fn prop_max_errors_serde_json(value in 1u32..=1000) {
266            let max = MaxErrors::try_new(value).unwrap();
267            let json = serde_json::to_string(&max).unwrap();
268            let parsed: MaxErrors = serde_json::from_str(&json).unwrap();
269            prop_assert_eq!(parsed.get(), value);
270        }
271
272        /// Property: Clone creates identical copy
273        #[test]
274        fn prop_max_errors_clone(value in 1u32..=1000) {
275            let max = MaxErrors::try_new(value).unwrap();
276            let cloned = max;
277            prop_assert_eq!(max, cloned);
278        }
279    }
280
281    // Edge Cases - MaxErrors
282    #[test]
283    fn test_max_errors_zero_rejected() {
284        assert!(MaxErrors::try_new(0).is_err());
285    }
286
287    #[test]
288    fn test_max_errors_default() {
289        assert_eq!(MaxErrors::DEFAULT, 3);
290    }
291
292    #[test]
293    fn test_max_errors_serde_json_zero_rejected() {
294        assert!(serde_json::from_str::<MaxErrors>("0").is_err());
295    }
296
297    // ============================================================================
298    // Property Tests - ThreadCount
299    // ============================================================================
300
301    proptest! {
302        /// Property: Any non-zero usize round-trips through ThreadCount
303        #[test]
304        fn prop_thread_count_valid_range(value in 1usize..=128) {
305            let threads = ThreadCount::new(value).unwrap();
306            prop_assert_eq!(threads.get(), value);
307        }
308
309        /// Property: from_value handles non-zero values correctly
310        #[test]
311        fn prop_thread_count_from_value(value in 1usize..=128) {
312            let threads = ThreadCount::from_value(value).unwrap();
313            prop_assert_eq!(threads.get(), value);
314        }
315
316        /// Property: Display shows exact value
317        #[test]
318        fn prop_thread_count_display(value in 1usize..=128) {
319            let threads = ThreadCount::new(value).unwrap();
320            prop_assert_eq!(threads.to_string(), value.to_string());
321        }
322
323        /// Property: Into<usize> conversion works
324        #[test]
325        fn prop_thread_count_into_usize(value in 1usize..=128) {
326            let threads = ThreadCount::new(value).unwrap();
327            let converted: usize = threads.into();
328            prop_assert_eq!(converted, value);
329        }
330
331        /// Property: FromStr parses valid numbers
332        #[test]
333        fn prop_thread_count_from_str(value in 1usize..=128) {
334            let s = value.to_string();
335            let threads: ThreadCount = s.parse().unwrap();
336            prop_assert_eq!(threads.get(), value);
337        }
338
339        /// Property: JSON serialization round-trips correctly
340        #[test]
341        fn prop_thread_count_serde_json(value in 1usize..=128) {
342            let threads = ThreadCount::new(value).unwrap();
343            let json = serde_json::to_string(&threads).unwrap();
344            let parsed: ThreadCount = serde_json::from_str(&json).unwrap();
345            prop_assert_eq!(parsed.get(), value);
346        }
347
348        /// Property: Clone creates identical copy
349        #[test]
350        fn prop_thread_count_clone(value in 1usize..=128) {
351            let threads = ThreadCount::new(value).unwrap();
352            let cloned = threads;
353            prop_assert_eq!(threads, cloned);
354        }
355    }
356
357    // Edge Cases - ThreadCount
358    #[test]
359    fn test_thread_count_default() {
360        assert_eq!(ThreadCount::default().get(), 1);
361    }
362
363    #[test]
364    fn test_thread_count_new_zero_rejected() {
365        // new() can't call num_cpus() in const context, so returns None for 0
366        assert!(ThreadCount::new(0).is_none());
367    }
368
369    #[test]
370    fn test_thread_count_from_value_zero_auto_detects() {
371        // from_value(0) should auto-detect CPU count
372        let threads = ThreadCount::from_value(0).unwrap();
373        assert!(threads.get() >= 1);
374    }
375
376    #[test]
377    fn test_thread_count_from_str_zero_auto_detects() {
378        // Parsing "0" should auto-detect CPU count
379        let threads: ThreadCount = "0".parse().unwrap();
380        assert!(threads.get() >= 1);
381    }
382
383    #[test]
384    fn test_thread_count_from_str_invalid() {
385        assert!("not_a_number".parse::<ThreadCount>().is_err());
386    }
387
388    #[test]
389    fn test_thread_count_serde_json_zero_auto_detects() {
390        // Deserializing 0 should auto-detect CPU count
391        let parsed: ThreadCount = serde_json::from_str("0").unwrap();
392        assert!(parsed.get() >= 1);
393    }
394}