use std::borrow::Cow;
pub const DEFAULT_MAX_CHARS: usize = 50_000;
pub const MAX_CHARS_CEILING: usize = 200_000;
pub const DEFAULT_MAX_RESULTS: usize = 20;
pub const MAX_RESULTS_CEILING: usize = 100;
pub const DEFAULT_MAX_BYTES: u64 = 8 * 1024 * 1024;
pub const MAX_BYTES_CEILING: u64 = 64 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limits {
max_chars: usize,
max_results: usize,
max_bytes: u64,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_chars: DEFAULT_MAX_CHARS,
max_results: DEFAULT_MAX_RESULTS,
max_bytes: DEFAULT_MAX_BYTES,
}
}
}
impl Limits {
pub fn with_max_chars(mut self, n: usize) -> Self {
self.max_chars = n.clamp(1, MAX_CHARS_CEILING);
self
}
pub fn with_max_results(mut self, n: usize) -> Self {
self.max_results = n.clamp(1, MAX_RESULTS_CEILING);
self
}
pub fn with_max_bytes(mut self, n: u64) -> Self {
self.max_bytes = n.clamp(1, MAX_BYTES_CEILING);
self
}
pub fn max_chars(&self) -> usize {
self.max_chars
}
pub fn max_results(&self) -> usize {
self.max_results
}
pub fn max_bytes(&self) -> u64 {
self.max_bytes
}
pub fn clamp_results(&self, requested: Option<usize>) -> usize {
requested.map_or(self.max_results, |n| n.clamp(1, self.max_results))
}
pub fn clamp_chars(&self, requested: Option<usize>) -> usize {
requested.map_or(self.max_chars, |n| n.clamp(1, self.max_chars))
}
pub fn bytes_allowed(&self, len: u64) -> bool {
len <= self.max_bytes
}
pub fn truncate<'a>(&self, text: &'a str) -> Cow<'a, str> {
truncate_chars(text, self.max_chars)
}
}
pub fn truncate_chars(text: &str, max_chars: usize) -> Cow<'_, str> {
match text.char_indices().nth(max_chars) {
None => Cow::Borrowed(text),
Some((cut, _)) => {
let total = text.chars().count();
Cow::Owned(format!(
"{}\n\n[truncated: showing the first {max_chars} of {total} characters]",
&text[..cut]
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults() {
let l = Limits::default();
assert_eq!(l.max_chars(), 50_000);
assert_eq!(l.max_results(), 20);
assert_eq!(l.max_bytes(), 8 * 1024 * 1024);
}
#[test]
fn setters_clamp_to_ceilings() {
let l = Limits::default()
.with_max_chars(1_000_000)
.with_max_results(10_000)
.with_max_bytes(u64::MAX);
assert_eq!(l.max_chars(), MAX_CHARS_CEILING);
assert_eq!(l.max_results(), MAX_RESULTS_CEILING);
assert_eq!(l.max_bytes(), MAX_BYTES_CEILING);
let z = Limits::default()
.with_max_chars(0)
.with_max_results(0)
.with_max_bytes(0);
assert_eq!((z.max_chars(), z.max_results(), z.max_bytes()), (1, 1, 1));
}
#[test]
fn requested_counts_are_clamped() {
let l = Limits::default();
assert_eq!(l.clamp_results(None), 20);
assert_eq!(l.clamp_results(Some(5)), 5);
assert_eq!(l.clamp_results(Some(0)), 1);
assert_eq!(l.clamp_results(Some(1_000)), 20);
assert_eq!(l.clamp_chars(Some(10)), 10);
assert_eq!(l.clamp_chars(Some(usize::MAX)), 50_000);
}
#[test]
fn byte_cap() {
let l = Limits::default().with_max_bytes(10);
assert!(l.bytes_allowed(10));
assert!(!l.bytes_allowed(11));
}
#[test]
fn short_text_is_borrowed_unchanged() {
assert!(matches!(truncate_chars("hello", 5), Cow::Borrowed("hello")));
assert!(matches!(truncate_chars("", 0), Cow::Borrowed("")));
}
#[test]
fn long_text_is_cut_with_a_note() {
let out = truncate_chars("hello world", 5);
assert!(out.starts_with("hello\n\n[truncated:"), "{out}");
assert!(out.contains("first 5 of 11 characters"), "{out}");
assert!(!out.contains("world"));
}
#[test]
fn cut_respects_multibyte_characters() {
let out = truncate_chars("żółw🐢abc", 4);
assert!(out.starts_with("żółw\n"), "{out}");
assert!(out.contains("first 4 of 8 characters"), "{out}");
let limited = Limits::default().with_max_chars(5).truncate("żółw🐢abc");
assert!(limited.starts_with("żółw🐢\n"), "{limited}");
}
}