nntp_proxy/types/config/
limits.rs1use nutype::nutype;
4use std::num::NonZeroUsize;
5
6#[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 pub const DEFAULT: usize = 10;
33
34 #[inline]
36 pub fn get(&self) -> usize {
37 self.into_inner()
38 }
39}
40
41#[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 pub const DEFAULT: u32 = 3;
66
67 #[inline]
69 pub fn get(&self) -> u32 {
70 self.into_inner()
71 }
72}
73
74#[repr(transparent)]
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
80pub struct ThreadCount(NonZeroUsize);
81
82impl ThreadCount {
83 pub const DEFAULT: Self = Self(NonZeroUsize::new(1).unwrap());
85
86 #[must_use]
92 pub const fn new(value: usize) -> Option<Self> {
93 if value == 0 {
94 None
98 } else {
99 match NonZeroUsize::new(value) {
100 Some(nz) => Some(Self(nz)),
101 None => None,
102 }
103 }
104 }
105
106 #[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 #[must_use]
122 #[inline]
123 pub const fn get(&self) -> usize {
124 self.0.get()
125 }
126
127 fn num_cpus() -> Self {
129 let count = std::thread::available_parallelism()
130 .map(|p| p.get())
131 .unwrap_or(1);
132 Self(NonZeroUsize::new(count).unwrap())
134 }
135}
136
137impl Default for ThreadCount {
138 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 proptest! {
196 #[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 #[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 #[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 #[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 #[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 proptest! {
249 #[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 #[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 #[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 #[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 #[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 proptest! {
302 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 assert!(ThreadCount::new(0).is_none());
367 }
368
369 #[test]
370 fn test_thread_count_from_value_zero_auto_detects() {
371 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 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 let parsed: ThreadCount = serde_json::from_str("0").unwrap();
392 assert!(parsed.get() >= 1);
393 }
394}