1use std::num::{NonZeroU64, NonZeroUsize};
7
8use serde::{Deserialize, Serialize};
9
10use super::bounded_number::impl_bounded_number;
11use super::text_newtype::impl_text_newtype;
12
13pub const DEFAULT_HEURISTICS_MAX_DEPTH: usize = 10;
15
16pub const MAX_TIMEOUT_SECONDS: u64 = 900;
34
35pub const MAX_HEURISTICS_DEPTH: usize = 64;
44
45#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
47#[error("max_concurrent_server_starts must be at least 1")]
48pub struct InvalidServerStartConcurrency;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(try_from = "usize", into = "usize")]
65pub struct ServerStartConcurrency(NonZeroUsize);
66
67impl ServerStartConcurrency {
68 pub const fn new(limit: usize) -> Result<Self, InvalidServerStartConcurrency> {
74 match NonZeroUsize::new(limit) {
75 Some(limit) => Ok(Self(limit)),
76 None => Err(InvalidServerStartConcurrency),
77 }
78 }
79
80 #[must_use]
82 pub const fn get(self) -> usize {
83 self.0.get()
84 }
85}
86
87impl_bounded_number!(
88 ServerStartConcurrency,
89 usize,
90 InvalidServerStartConcurrency,
91 default = 8,
92 "Eight servers at a time.",
93 into = ServerStartConcurrency::get
94);
95
96#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
98#[error("heuristics_max_depth ({value}) exceeds the maximum of {max}")]
99pub struct InvalidSearchDepth {
100 pub value: usize,
102 pub max: usize,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
122#[serde(try_from = "usize", into = "usize")]
123pub struct SearchDepth(u8);
124
125impl SearchDepth {
126 pub const fn new(depth: usize) -> Result<Self, InvalidSearchDepth> {
132 const { assert!(MAX_HEURISTICS_DEPTH <= u8::MAX as usize) };
133 if depth > MAX_HEURISTICS_DEPTH {
134 return Err(InvalidSearchDepth {
135 value: depth,
136 max: MAX_HEURISTICS_DEPTH,
137 });
138 }
139 #[expect(
140 clippy::cast_possible_truncation,
141 reason = "bounded by MAX_HEURISTICS_DEPTH, asserted to fit u8 above"
142 )]
143 Ok(Self(depth as u8))
144 }
145
146 #[must_use]
148 pub const fn get(self) -> usize {
149 self.0 as usize
150 }
151}
152
153impl_bounded_number!(
154 SearchDepth,
155 usize,
156 InvalidSearchDepth,
157 default = DEFAULT_HEURISTICS_MAX_DEPTH,
158 "The built-in default, [`DEFAULT_HEURISTICS_MAX_DEPTH`].",
159 into = SearchDepth::get
160);
161
162pub const MAX_FILE_SIZE_LIMIT: u64 = 1 << 30;
168
169#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
171#[error(
172 "max_file_size ({value}) exceeds the hard cap of {max} bytes; use a lower value, or 0 to \
173 disable the per-file limit"
174)]
175pub struct InvalidSizeLimit {
176 pub value: u64,
178 pub max: u64,
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
202#[serde(try_from = "u64", into = "u64")]
203pub struct SizeLimit(Option<NonZeroU64>);
204
205impl SizeLimit {
206 pub const UNLIMITED: Self = Self(None);
208
209 pub const fn new(bytes: u64) -> Result<Self, InvalidSizeLimit> {
215 if bytes > MAX_FILE_SIZE_LIMIT {
216 return Err(InvalidSizeLimit {
217 value: bytes,
218 max: MAX_FILE_SIZE_LIMIT,
219 });
220 }
221 Ok(Self(NonZeroU64::new(bytes)))
222 }
223
224 #[must_use]
231 pub const fn from_static(bytes: u64) -> Self {
232 match Self::new(bytes) {
233 Ok(limit) => limit,
234 Err(_) => panic!("size limit above MAX_FILE_SIZE_LIMIT"),
235 }
236 }
237
238 #[must_use]
240 pub const fn get(self) -> Option<NonZeroU64> {
241 self.0
242 }
243
244 #[must_use]
246 pub const fn admits(self, bytes: u64) -> bool {
247 match self.0 {
248 Some(max) => bytes <= max.get(),
249 None => true,
250 }
251 }
252
253 #[must_use]
257 pub const fn read_cap(self) -> u64 {
258 match self.0 {
259 Some(max) => Self::read_cap_for(max),
260 None => u64::MAX,
261 }
262 }
263
264 #[must_use]
266 pub(crate) const fn read_cap_for(max: NonZeroU64) -> u64 {
267 max.get().saturating_add(1)
268 }
269}
270
271impl_bounded_number!(
272 SizeLimit,
273 u64,
274 InvalidSizeLimit,
275 default = 10 * 1024 * 1024,
276 "The built-in default, 10 MiB.",
277 into = |limit: SizeLimit| limit.0.map_or_default(NonZeroU64::get)
278);
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
294#[serde(from = "usize", into = "usize")]
295pub struct DocumentLimit(Option<NonZeroUsize>);
296
297impl DocumentLimit {
298 pub const UNLIMITED: Self = Self(None);
300
301 #[must_use]
303 pub const fn new(count: usize) -> Self {
304 Self(NonZeroUsize::new(count))
305 }
306
307 #[must_use]
309 pub const fn get(self) -> Option<NonZeroUsize> {
310 self.0
311 }
312}
313
314impl_bounded_number!(
315 DocumentLimit,
316 usize,
317 infallible,
318 default = 100,
319 "The built-in default, 100 documents.",
320 into = |limit: DocumentLimit| limit.0.map_or_default(NonZeroUsize::get)
321);
322
323#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
325#[non_exhaustive]
326pub enum InvalidBoundedText {
327 #[error("cannot be empty")]
329 Blank,
330 #[error("exceeds the maximum of {max} bytes ({len} given)")]
332 TooLong {
333 len: usize,
335 max: usize,
337 },
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
356#[serde(try_from = "String", into = "String")]
357pub struct BoundedText<const MAX: usize>(String);
358
359impl<const MAX: usize> BoundedText<MAX> {
360 pub fn new(text: impl Into<String>) -> Result<Self, InvalidBoundedText> {
367 let text = text.into();
368 if text.trim().is_empty() {
369 return Err(InvalidBoundedText::Blank);
370 }
371 let len = text.len();
372 if len > MAX {
373 return Err(InvalidBoundedText::TooLong { len, max: MAX });
374 }
375 Ok(Self(text))
376 }
377}
378
379impl_text_newtype!(@impl [const MAX: usize] BoundedText<MAX>, InvalidBoundedText);
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_bounded_text_blank_is_reported_before_length() {
387 assert_eq!(BoundedText::<4>::new(""), Err(InvalidBoundedText::Blank));
388 assert_eq!(
389 BoundedText::<4>::new(" "),
390 Err(InvalidBoundedText::Blank)
391 );
392 }
393
394 #[test]
395 fn test_bounded_text_counts_bytes_not_chars() {
396 assert!(BoundedText::<4>::new("éé").is_ok());
397 assert_eq!(
398 BoundedText::<4>::new("ééé"),
399 Err(InvalidBoundedText::TooLong { len: 6, max: 4 })
400 );
401 assert_eq!(
402 BoundedText::<4>::new("ééé").unwrap_err().to_string(),
403 "exceeds the maximum of 4 bytes (6 given)"
404 );
405 }
406
407 #[test]
408 fn test_bounded_text_has_the_shared_text_conversions() {
409 let text: BoundedText<8> = "hello".parse().unwrap();
410 assert_eq!(text.as_ref() as &str, "hello");
411 assert_eq!(std::borrow::Borrow::<str>::borrow(&text), "hello");
412 assert_eq!(text.to_string(), "hello");
413 assert_eq!(String::from(text), "hello");
414 assert!("123456789".parse::<BoundedText<8>>().is_err());
415 }
416
417 #[test]
418 fn test_bounded_text_serde() {
419 #[derive(Debug, Serialize, Deserialize, PartialEq)]
420 struct Holder {
421 text: BoundedText<5>,
422 }
423
424 let holder: Holder = toml::from_str(r#"text = "abc""#).unwrap();
425 assert_eq!(holder.text, "abc");
426 assert_eq!(toml::to_string(&holder).unwrap().trim(), r#"text = "abc""#);
427 assert!(toml::from_str::<Holder>(r#"text = "abcdef""#).is_err());
428 assert!(toml::from_str::<Holder>(r#"text = " ""#).is_err());
429 }
430
431 #[test]
432 fn test_size_limit_zero_is_unlimited_and_cap_is_enforced() {
433 assert_eq!(SizeLimit::new(0), Ok(SizeLimit::UNLIMITED));
434 assert!(SizeLimit::new(MAX_FILE_SIZE_LIMIT).is_ok());
435 assert_eq!(
436 SizeLimit::new(MAX_FILE_SIZE_LIMIT + 1),
437 Err(InvalidSizeLimit {
438 value: MAX_FILE_SIZE_LIMIT + 1,
439 max: MAX_FILE_SIZE_LIMIT
440 })
441 );
442 }
443
444 #[test]
445 fn test_size_limit_admits_up_to_and_including_the_limit() {
446 let limit = SizeLimit::from_static(4);
447 assert!(limit.admits(0));
448 assert!(limit.admits(4));
449 assert!(!limit.admits(5));
450 assert!(SizeLimit::UNLIMITED.admits(u64::MAX));
451 }
452
453 #[test]
454 fn test_read_cap_for_is_the_limit_plus_one_and_saturates() {
455 assert_eq!(SizeLimit::read_cap_for(NonZeroU64::new(100).unwrap()), 101);
456 assert_eq!(SizeLimit::read_cap_for(NonZeroU64::MAX), u64::MAX);
457 }
458
459 #[test]
460 fn test_server_start_concurrency_rejects_zero_with_a_typed_error() {
461 assert_eq!(
462 ServerStartConcurrency::new(0),
463 Err(InvalidServerStartConcurrency)
464 );
465 assert_eq!(ServerStartConcurrency::new(3).unwrap().get(), 3);
466 }
467
468 #[test]
469 fn test_size_limit_read_cap_is_limit_plus_one_or_unbounded() {
470 assert_eq!(SizeLimit::from_static(100).read_cap(), 101);
471 assert_eq!(
472 SizeLimit::from_static(MAX_FILE_SIZE_LIMIT).read_cap(),
473 MAX_FILE_SIZE_LIMIT + 1
474 );
475 assert_eq!(SizeLimit::UNLIMITED.read_cap(), u64::MAX);
476 }
477
478 #[test]
479 fn test_size_limit_and_document_limit_serde_use_zero_for_unlimited() {
480 #[derive(Debug, Serialize, Deserialize, PartialEq)]
481 struct Holder {
482 size: SizeLimit,
483 docs: DocumentLimit,
484 }
485
486 let holder: Holder = toml::from_str("size = 0\ndocs = 0").unwrap();
487 assert_eq!(holder.size, SizeLimit::UNLIMITED);
488 assert_eq!(holder.docs, DocumentLimit::UNLIMITED);
489 assert_eq!(
490 toml::to_string(&holder).unwrap().trim(),
491 "size = 0\ndocs = 0"
492 );
493 let holder: Holder = toml::from_str("size = 7\ndocs = 3").unwrap();
494 assert_eq!(holder.size.get().unwrap().get(), 7);
495 assert_eq!(holder.docs.get().unwrap().get(), 3);
496 assert!(toml::from_str::<Holder>("size = 1073741825\ndocs = 1").is_err());
497 assert!(toml::from_str::<Holder>("size = 1\ndocs = -1").is_err());
498 }
499
500 #[test]
501 fn test_defaults_match_documented_values() {
502 assert_eq!(SizeLimit::default().get().unwrap().get(), 10 * 1024 * 1024);
503 assert_eq!(DocumentLimit::default().get().unwrap().get(), 100);
504 }
505
506 #[test]
507 fn test_search_depth_bounds() {
508 assert_eq!(SearchDepth::new(0).unwrap().get(), 0);
509 assert_eq!(
510 SearchDepth::new(MAX_HEURISTICS_DEPTH).unwrap().get(),
511 MAX_HEURISTICS_DEPTH
512 );
513 assert!(SearchDepth::new(MAX_HEURISTICS_DEPTH + 1).is_err());
514 assert_eq!(SearchDepth::default().get(), DEFAULT_HEURISTICS_MAX_DEPTH);
515 }
516
517 #[test]
518 fn test_search_depth_error_reports_range() {
519 let err = SearchDepth::try_from(999_999).unwrap_err();
520 assert_eq!(
521 err.to_string(),
522 "heuristics_max_depth (999999) exceeds the maximum of 64"
523 );
524 }
525
526 #[test]
527 fn test_search_depth_serde() {
528 #[derive(Debug, Serialize, Deserialize, PartialEq)]
529 struct Holder {
530 depth: SearchDepth,
531 }
532
533 let holder: Holder = toml::from_str("depth = 5").unwrap();
534 assert_eq!(holder.depth.get(), 5);
535 assert_eq!(toml::to_string(&holder).unwrap().trim(), "depth = 5");
536 assert!(toml::from_str::<Holder>("depth = 65").is_err());
537 assert!(toml::from_str::<Holder>("depth = -1").is_err());
538 }
539}