use std::num::{NonZeroU64, NonZeroUsize};
use serde::{Deserialize, Serialize};
use super::bounded_number::impl_bounded_number;
use super::text_newtype::impl_text_newtype;
pub const DEFAULT_HEURISTICS_MAX_DEPTH: usize = 10;
pub const MAX_TIMEOUT_SECONDS: u64 = 900;
pub const MAX_HEURISTICS_DEPTH: usize = 64;
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
#[error("max_concurrent_server_starts must be at least 1")]
pub struct InvalidServerStartConcurrency;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "usize", into = "usize")]
pub struct ServerStartConcurrency(NonZeroUsize);
impl ServerStartConcurrency {
pub const fn new(limit: usize) -> Result<Self, InvalidServerStartConcurrency> {
match NonZeroUsize::new(limit) {
Some(limit) => Ok(Self(limit)),
None => Err(InvalidServerStartConcurrency),
}
}
#[must_use]
pub const fn get(self) -> usize {
self.0.get()
}
}
impl_bounded_number!(
ServerStartConcurrency,
usize,
InvalidServerStartConcurrency,
default = 8,
"Eight servers at a time.",
into = ServerStartConcurrency::get
);
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
#[error("heuristics_max_depth ({value}) exceeds the maximum of {max}")]
pub struct InvalidSearchDepth {
pub value: usize,
pub max: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "usize", into = "usize")]
pub struct SearchDepth(u8);
impl SearchDepth {
pub const fn new(depth: usize) -> Result<Self, InvalidSearchDepth> {
const { assert!(MAX_HEURISTICS_DEPTH <= u8::MAX as usize) };
if depth > MAX_HEURISTICS_DEPTH {
return Err(InvalidSearchDepth {
value: depth,
max: MAX_HEURISTICS_DEPTH,
});
}
#[expect(
clippy::cast_possible_truncation,
reason = "bounded by MAX_HEURISTICS_DEPTH, asserted to fit u8 above"
)]
Ok(Self(depth as u8))
}
#[must_use]
pub const fn get(self) -> usize {
self.0 as usize
}
}
impl_bounded_number!(
SearchDepth,
usize,
InvalidSearchDepth,
default = DEFAULT_HEURISTICS_MAX_DEPTH,
"The built-in default, [`DEFAULT_HEURISTICS_MAX_DEPTH`].",
into = SearchDepth::get
);
pub const MAX_FILE_SIZE_LIMIT: u64 = 1 << 30;
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
#[error(
"max_file_size ({value}) exceeds the hard cap of {max} bytes; use a lower value, or 0 to \
disable the per-file limit"
)]
pub struct InvalidSizeLimit {
pub value: u64,
pub max: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "u64", into = "u64")]
pub struct SizeLimit(Option<NonZeroU64>);
impl SizeLimit {
pub const UNLIMITED: Self = Self(None);
pub const fn new(bytes: u64) -> Result<Self, InvalidSizeLimit> {
if bytes > MAX_FILE_SIZE_LIMIT {
return Err(InvalidSizeLimit {
value: bytes,
max: MAX_FILE_SIZE_LIMIT,
});
}
Ok(Self(NonZeroU64::new(bytes)))
}
#[must_use]
pub const fn from_static(bytes: u64) -> Self {
match Self::new(bytes) {
Ok(limit) => limit,
Err(_) => panic!("size limit above MAX_FILE_SIZE_LIMIT"),
}
}
#[must_use]
pub const fn get(self) -> Option<NonZeroU64> {
self.0
}
#[must_use]
pub const fn admits(self, bytes: u64) -> bool {
match self.0 {
Some(max) => bytes <= max.get(),
None => true,
}
}
#[must_use]
pub const fn read_cap(self) -> u64 {
match self.0 {
Some(max) => Self::read_cap_for(max),
None => u64::MAX,
}
}
#[must_use]
pub(crate) const fn read_cap_for(max: NonZeroU64) -> u64 {
max.get().saturating_add(1)
}
}
impl_bounded_number!(
SizeLimit,
u64,
InvalidSizeLimit,
default = 10 * 1024 * 1024,
"The built-in default, 10 MiB.",
into = |limit: SizeLimit| limit.0.map_or_default(NonZeroU64::get)
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "usize", into = "usize")]
pub struct DocumentLimit(Option<NonZeroUsize>);
impl DocumentLimit {
pub const UNLIMITED: Self = Self(None);
#[must_use]
pub const fn new(count: usize) -> Self {
Self(NonZeroUsize::new(count))
}
#[must_use]
pub const fn get(self) -> Option<NonZeroUsize> {
self.0
}
}
impl_bounded_number!(
DocumentLimit,
usize,
infallible,
default = 100,
"The built-in default, 100 documents.",
into = |limit: DocumentLimit| limit.0.map_or_default(NonZeroUsize::get)
);
#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvalidBoundedText {
#[error("cannot be empty")]
Blank,
#[error("exceeds the maximum of {max} bytes ({len} given)")]
TooLong {
len: usize,
max: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct BoundedText<const MAX: usize>(String);
impl<const MAX: usize> BoundedText<MAX> {
pub fn new(text: impl Into<String>) -> Result<Self, InvalidBoundedText> {
let text = text.into();
if text.trim().is_empty() {
return Err(InvalidBoundedText::Blank);
}
let len = text.len();
if len > MAX {
return Err(InvalidBoundedText::TooLong { len, max: MAX });
}
Ok(Self(text))
}
}
impl_text_newtype!(@impl [const MAX: usize] BoundedText<MAX>, InvalidBoundedText);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bounded_text_blank_is_reported_before_length() {
assert_eq!(BoundedText::<4>::new(""), Err(InvalidBoundedText::Blank));
assert_eq!(
BoundedText::<4>::new(" "),
Err(InvalidBoundedText::Blank)
);
}
#[test]
fn test_bounded_text_counts_bytes_not_chars() {
assert!(BoundedText::<4>::new("éé").is_ok());
assert_eq!(
BoundedText::<4>::new("ééé"),
Err(InvalidBoundedText::TooLong { len: 6, max: 4 })
);
assert_eq!(
BoundedText::<4>::new("ééé").unwrap_err().to_string(),
"exceeds the maximum of 4 bytes (6 given)"
);
}
#[test]
fn test_bounded_text_has_the_shared_text_conversions() {
let text: BoundedText<8> = "hello".parse().unwrap();
assert_eq!(text.as_ref() as &str, "hello");
assert_eq!(std::borrow::Borrow::<str>::borrow(&text), "hello");
assert_eq!(text.to_string(), "hello");
assert_eq!(String::from(text), "hello");
assert!("123456789".parse::<BoundedText<8>>().is_err());
}
#[test]
fn test_bounded_text_serde() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Holder {
text: BoundedText<5>,
}
let holder: Holder = toml::from_str(r#"text = "abc""#).unwrap();
assert_eq!(holder.text, "abc");
assert_eq!(toml::to_string(&holder).unwrap().trim(), r#"text = "abc""#);
assert!(toml::from_str::<Holder>(r#"text = "abcdef""#).is_err());
assert!(toml::from_str::<Holder>(r#"text = " ""#).is_err());
}
#[test]
fn test_size_limit_zero_is_unlimited_and_cap_is_enforced() {
assert_eq!(SizeLimit::new(0), Ok(SizeLimit::UNLIMITED));
assert!(SizeLimit::new(MAX_FILE_SIZE_LIMIT).is_ok());
assert_eq!(
SizeLimit::new(MAX_FILE_SIZE_LIMIT + 1),
Err(InvalidSizeLimit {
value: MAX_FILE_SIZE_LIMIT + 1,
max: MAX_FILE_SIZE_LIMIT
})
);
}
#[test]
fn test_size_limit_admits_up_to_and_including_the_limit() {
let limit = SizeLimit::from_static(4);
assert!(limit.admits(0));
assert!(limit.admits(4));
assert!(!limit.admits(5));
assert!(SizeLimit::UNLIMITED.admits(u64::MAX));
}
#[test]
fn test_read_cap_for_is_the_limit_plus_one_and_saturates() {
assert_eq!(SizeLimit::read_cap_for(NonZeroU64::new(100).unwrap()), 101);
assert_eq!(SizeLimit::read_cap_for(NonZeroU64::MAX), u64::MAX);
}
#[test]
fn test_server_start_concurrency_rejects_zero_with_a_typed_error() {
assert_eq!(
ServerStartConcurrency::new(0),
Err(InvalidServerStartConcurrency)
);
assert_eq!(ServerStartConcurrency::new(3).unwrap().get(), 3);
}
#[test]
fn test_size_limit_read_cap_is_limit_plus_one_or_unbounded() {
assert_eq!(SizeLimit::from_static(100).read_cap(), 101);
assert_eq!(
SizeLimit::from_static(MAX_FILE_SIZE_LIMIT).read_cap(),
MAX_FILE_SIZE_LIMIT + 1
);
assert_eq!(SizeLimit::UNLIMITED.read_cap(), u64::MAX);
}
#[test]
fn test_size_limit_and_document_limit_serde_use_zero_for_unlimited() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Holder {
size: SizeLimit,
docs: DocumentLimit,
}
let holder: Holder = toml::from_str("size = 0\ndocs = 0").unwrap();
assert_eq!(holder.size, SizeLimit::UNLIMITED);
assert_eq!(holder.docs, DocumentLimit::UNLIMITED);
assert_eq!(
toml::to_string(&holder).unwrap().trim(),
"size = 0\ndocs = 0"
);
let holder: Holder = toml::from_str("size = 7\ndocs = 3").unwrap();
assert_eq!(holder.size.get().unwrap().get(), 7);
assert_eq!(holder.docs.get().unwrap().get(), 3);
assert!(toml::from_str::<Holder>("size = 1073741825\ndocs = 1").is_err());
assert!(toml::from_str::<Holder>("size = 1\ndocs = -1").is_err());
}
#[test]
fn test_defaults_match_documented_values() {
assert_eq!(SizeLimit::default().get().unwrap().get(), 10 * 1024 * 1024);
assert_eq!(DocumentLimit::default().get().unwrap().get(), 100);
}
#[test]
fn test_search_depth_bounds() {
assert_eq!(SearchDepth::new(0).unwrap().get(), 0);
assert_eq!(
SearchDepth::new(MAX_HEURISTICS_DEPTH).unwrap().get(),
MAX_HEURISTICS_DEPTH
);
assert!(SearchDepth::new(MAX_HEURISTICS_DEPTH + 1).is_err());
assert_eq!(SearchDepth::default().get(), DEFAULT_HEURISTICS_MAX_DEPTH);
}
#[test]
fn test_search_depth_error_reports_range() {
let err = SearchDepth::try_from(999_999).unwrap_err();
assert_eq!(
err.to_string(),
"heuristics_max_depth (999999) exceeds the maximum of 64"
);
}
#[test]
fn test_search_depth_serde() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Holder {
depth: SearchDepth,
}
let holder: Holder = toml::from_str("depth = 5").unwrap();
assert_eq!(holder.depth.get(), 5);
assert_eq!(toml::to_string(&holder).unwrap().trim(), "depth = 5");
assert!(toml::from_str::<Holder>("depth = 65").is_err());
assert!(toml::from_str::<Holder>("depth = -1").is_err());
}
}