use serde_json::Number;
use crate::errors::Refusal;
pub const BATCH_ID_MAX_LEN: usize = 128;
pub fn is_batch_id(batch_id: &str) -> bool {
!batch_id.is_empty()
&& batch_id.len() <= BATCH_ID_MAX_LEN
&& batch_id.bytes().all(|b| (0x20..=0x7e).contains(&b))
}
pub fn check_batch_id(batch_id: &str) -> Result<(), Refusal> {
if is_batch_id(batch_id) {
Ok(())
} else {
Err(Refusal::invalid_params(
"batch_id",
"batch_id is 1-128 printable ASCII characters",
))
}
}
pub fn bounded(
count: usize,
min: usize,
max: usize,
field: &str,
what: &str,
) -> Result<(), Refusal> {
if count < min {
Err(Refusal::over_limit(
field,
min as u64,
format!("{count} {what}; at least {min}"),
))
} else if count > max {
Err(Refusal::over_limit(
field,
max as u64,
format!("{count} {what}; at most {max}"),
))
} else {
Ok(())
}
}
pub fn check_request_size(len: usize, max_request_bytes: u64) -> Result<(), Refusal> {
if len as u64 > max_request_bytes {
Err(Refusal::over_limit(
"params",
max_request_bytes,
format!("the request is {len} bytes; at most {max_request_bytes}"),
))
} else {
Ok(())
}
}
pub fn check_max_cost_usd(max_cost_usd: Option<&Number>) -> Result<(), Refusal> {
match max_cost_usd {
Some(max) if max.as_f64().is_none_or(|usd| usd < 0.0) => Err(Refusal::invalid_params(
"max_cost_usd",
"max_cost_usd is a number of USD, zero or more",
)),
_ => Ok(()),
}
}
pub fn effective_ceiling(recorded: Option<f64>, sent: Option<f64>) -> Option<f64> {
match (recorded, sent) {
(Some(recorded), Some(sent)) => Some(recorded.min(sent)),
(one, None) | (None, one) => one,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CatalogRow<'a> {
Absent,
Kindless,
Kind(&'a str),
}
pub fn check_catalog_row(
model: &str,
row: CatalogRow<'_>,
expected_kind: &str,
wrong_kind: impl FnOnce(&str, Option<&str>) -> Refusal,
) -> Result<(), Refusal> {
match row {
CatalogRow::Absent => Err(Refusal::model_unknown(model)),
CatalogRow::Kind(kind) if kind == expected_kind => Ok(()),
CatalogRow::Kind(kind) => Err(wrong_kind(model, Some(kind))),
CatalogRow::Kindless => Err(wrong_kind(model, None)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::errors::{MODEL_NOT_EXTRACTOR, MODEL_UNKNOWN};
#[test]
fn batch_ids_are_printable_ascii_up_to_128() {
assert!(is_batch_id("b-1"));
assert!(!is_batch_id(""));
assert!(!is_batch_id("tab\there"));
assert!(!is_batch_id(&"x".repeat(129)));
assert!(is_batch_id(&"x".repeat(128)));
}
#[test]
fn the_ceiling_tightens_and_never_loosens() {
let mut recorded = None;
let mut worked_under = Vec::new();
for sent in [5.0, 1.0, 5.0] {
recorded = effective_ceiling(recorded, Some(sent));
worked_under.push(recorded);
}
assert_eq!(worked_under, [Some(5.0), Some(1.0), Some(1.0)]);
assert_eq!(effective_ceiling(None, None), None);
}
#[test]
fn a_kindless_row_is_not_a_missing_row() {
let wrong = |model: &str, kind: Option<&str>| Refusal::model_not_extractor(model, kind);
let absent = check_catalog_row("m", CatalogRow::Absent, "extraction", wrong).unwrap_err();
assert_eq!(absent.code, MODEL_UNKNOWN);
let kindless =
check_catalog_row("m", CatalogRow::Kindless, "extraction", wrong).unwrap_err();
assert_eq!(kindless.code, MODEL_NOT_EXTRACTOR);
assert_eq!(kindless.detail.kind, None);
assert!(
check_catalog_row("m", CatalogRow::Kind("extraction"), "extraction", wrong).is_ok()
);
}
}