use super::FormErrors;
use serde_json::Value;
#[allow(clippy::too_many_arguments)]
pub fn check_value(
field: &str,
value: &Value,
max_length: Option<usize>,
min_length: Option<usize>,
min: Option<i64>,
max: Option<i64>,
choices: Option<&[(&str, &str)]>,
errors: &mut FormErrors,
) {
if let Some(s) = value.as_str() {
let len = s.chars().count();
if let Some(m) = max_length {
if len > m {
errors.add(
field,
format!("Ensure this value has at most {m} characters."),
);
}
}
if let Some(m) = min_length {
if !s.is_empty() && len < m {
errors.add(
field,
format!("Ensure this value has at least {m} characters."),
);
}
}
if let Some(cs) = choices {
if !s.is_empty() && !cs.iter().any(|(v, _)| *v == s) {
errors.add(field, "Select a valid choice.");
}
}
} else if value.is_number() {
let below = |m: i64| match value.as_i64() {
Some(n) => n < m,
None => value.as_f64().is_some_and(|n| n < m as f64),
};
let above = |m: i64| match value.as_i64() {
Some(n) => n > m,
None => value.as_f64().is_some_and(|n| n > m as f64),
};
if let Some(m) = min.filter(|&m| below(m)) {
errors.add(field, format!("Ensure this value is ≥ {m}."));
}
if let Some(m) = max.filter(|&m| above(m)) {
errors.add(field, format!("Ensure this value is ≤ {m}."));
}
}
}
#[doc(hidden)]
#[diagnostic::on_unimplemented(
message = "`max_length` / `min_length` need a string field, not `{Self}`"
)]
pub trait LengthBounded {}
impl LengthBounded for String {}
impl LengthBounded for str {}
impl LengthBounded for Box<str> {}
impl LengthBounded for std::borrow::Cow<'_, str> {}
impl<T: LengthBounded + ?Sized> LengthBounded for &T {}
impl<T: LengthBounded> LengthBounded for Option<T> {}
#[doc(hidden)]
#[diagnostic::on_unimplemented(message = "`min` / `max` need a numeric field, not `{Self}`")]
pub trait NumberBounded {}
macro_rules! number_bounded {
($($t:ty),*) => { $(impl NumberBounded for $t {})* };
}
number_bounded!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, f32, f64);
impl<T: NumberBounded + ?Sized> NumberBounded for &T {}
impl<T: NumberBounded> NumberBounded for Option<T> {}
#[doc(hidden)]
pub const fn assert_length_bounded<T: LengthBounded + ?Sized>() {}
#[doc(hidden)]
pub const fn assert_number_bounded<T: NumberBounded + ?Sized>() {}
#[cfg(all(doctest, feature = "serializer"))]
pub struct BoundsMatchTheFieldType;
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn max_length_flags_too_long_string() {
let mut e = FormErrors::default();
check_value(
"name",
&json!("abcdef"),
Some(3),
None,
None,
None,
None,
&mut e,
);
assert_eq!(e.get("name").len(), 1);
assert!(e.get("name")[0].contains("at most 3 characters"));
}
#[test]
fn min_length_skips_empty_but_flags_short() {
let mut e = FormErrors::default();
check_value("name", &json!(""), None, Some(3), None, None, None, &mut e);
assert!(
e.is_empty(),
"empty string is 'not provided', not a length error"
);
check_value(
"name",
&json!("ab"),
None,
Some(3),
None,
None,
None,
&mut e,
);
assert!(e.get("name")[0].contains("at least 3 characters"));
}
#[test]
fn min_max_bound_integers() {
let mut e = FormErrors::default();
check_value("n", &json!(5), None, None, Some(0), Some(3), None, &mut e);
assert!(e.get("n")[0].contains("≤ 3"));
let mut e = FormErrors::default();
check_value("n", &json!(-1), None, None, Some(0), Some(3), None, &mut e);
assert!(e.get("n")[0].contains("≥ 0"));
}
#[test]
fn choices_rejects_value_not_in_set() {
let mut e = FormErrors::default();
let choices: &[(&str, &str)] = &[("draft", "Draft"), ("published", "Published")];
check_value(
"status",
&json!("archived"),
None,
None,
None,
None,
Some(choices),
&mut e,
);
assert!(e.get("status")[0].contains("valid choice"));
let mut e = FormErrors::default();
check_value(
"status",
&json!("draft"),
None,
None,
None,
None,
Some(choices),
&mut e,
);
assert!(e.is_empty());
}
#[test]
fn null_and_non_scalar_are_skipped() {
let mut e = FormErrors::default();
check_value(
"x",
&json!(null),
Some(1),
Some(1),
Some(1),
Some(1),
None,
&mut e,
);
check_value(
"x",
&json!({"a": 1}),
Some(1),
None,
None,
None,
None,
&mut e,
);
assert!(e.is_empty());
}
}