use std::collections::HashMap;
use std::ops::RangeInclusive;
use serde_json::{Value, json};
use super::{Validator, ValidatorOptions};
use crate::errors::{ErrorType, Errors};
#[derive(Debug, Clone, Default)]
pub struct LengthValidator {
minimum: Option<usize>,
maximum: Option<usize>,
exact: Option<usize>,
message: Option<String>,
too_short: Option<String>,
too_long: Option<String>,
wrong_length: Option<String>,
pub(crate) options: ValidatorOptions,
}
impl LengthValidator {
#[must_use]
pub fn new() -> Self {
Self::default()
}
crate::validations::impl_common_validator_methods!();
#[must_use]
pub fn minimum(mut self, minimum: usize) -> Self {
self.minimum = Some(minimum);
self
}
#[must_use]
pub fn maximum(mut self, maximum: usize) -> Self {
self.maximum = Some(maximum);
self
}
#[must_use]
pub fn is(mut self, exact: usize) -> Self {
self.exact = Some(exact);
self
}
#[must_use]
pub fn in_range(mut self, range: RangeInclusive<usize>) -> Self {
self.minimum = Some(*range.start());
self.maximum = Some(*range.end());
self
}
#[must_use]
pub fn message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
#[must_use]
pub fn too_short_message(mut self, message: impl Into<String>) -> Self {
self.too_short = Some(message.into());
self
}
#[must_use]
pub fn too_long_message(mut self, message: impl Into<String>) -> Self {
self.too_long = Some(message.into());
self
}
#[must_use]
pub fn wrong_length_message(mut self, message: impl Into<String>) -> Self {
self.wrong_length = Some(message.into());
self
}
fn value_length(value: Option<&Value>) -> usize {
match value {
None | Some(Value::Null) => 0,
Some(Value::String(text)) => text.chars().count(),
Some(Value::Array(values)) => values.len(),
Some(Value::Object(values)) => values.len(),
Some(Value::Bool(flag)) => flag.to_string().chars().count(),
Some(Value::Number(number)) => number.to_string().chars().count(),
}
}
fn build_message(template: &str, count: usize) -> String {
template.replace("%{count}", &count.to_string())
}
fn details(count: usize) -> HashMap<String, Value> {
HashMap::from([(String::from("count"), json!(count))])
}
fn too_short_error_message(&self, count: usize) -> String {
let template = self
.too_short
.as_deref()
.or(self.message.as_deref())
.unwrap_or("is too short (minimum is %{count} characters)");
Self::build_message(template, count)
}
fn too_long_error_message(&self, count: usize) -> String {
let template = self
.too_long
.as_deref()
.or(self.message.as_deref())
.unwrap_or("is too long (maximum is %{count} characters)");
Self::build_message(template, count)
}
fn wrong_length_error_message(&self, count: usize) -> String {
let template = self
.wrong_length
.as_deref()
.or(self.message.as_deref())
.unwrap_or("is the wrong length (should be %{count} characters)");
Self::build_message(template, count)
}
}
impl Validator for LengthValidator {
fn validate(&self, attribute: &str, value: Option<&Value>, errors: &mut Errors) {
let length = Self::value_length(value);
if let Some(exact) = self.exact
&& length != exact
{
errors.add_with_details(
attribute,
ErrorType::WrongLength,
self.wrong_length_error_message(exact),
Self::details(exact),
);
return;
}
if let Some(minimum) = self.minimum
&& length < minimum
{
errors.add_with_details(
attribute,
ErrorType::TooShort,
self.too_short_error_message(minimum),
Self::details(minimum),
);
}
if let Some(maximum) = self.maximum
&& length > maximum
{
errors.add_with_details(
attribute,
ErrorType::TooLong,
self.too_long_error_message(maximum),
Self::details(maximum),
);
}
}
fn name(&self) -> &str {
"length"
}
fn options(&self) -> &ValidatorOptions {
&self.options
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::LengthValidator;
use crate::{
errors::{ErrorType, Errors},
validations::{ValidationSet, Validator},
};
fn validate_length(validator: LengthValidator, value: Option<serde_json::Value>) -> Errors {
let mut errors = Errors::new();
validator.validate("field", value.as_ref(), &mut errors);
errors
}
#[test]
fn minimum_fails_for_short_string() {
let validator = LengthValidator::new().minimum(3);
let mut errors = Errors::new();
validator.validate("name", Some(&json!("Al")), &mut errors);
assert_eq!(errors.on("name")[0].error_type, ErrorType::TooShort);
}
#[test]
fn maximum_fails_for_long_string() {
let validator = LengthValidator::new().maximum(5);
let mut errors = Errors::new();
validator.validate("name", Some(&json!("Roberto")), &mut errors);
assert_eq!(errors.on("name")[0].error_type, ErrorType::TooLong);
}
#[test]
fn exact_length_fails_when_mismatched() {
let validator = LengthValidator::new().is(4);
let mut errors = Errors::new();
validator.validate("code", Some(&json!("abc")), &mut errors);
assert_eq!(errors.on("code")[0].error_type, ErrorType::WrongLength);
}
#[test]
fn inclusive_range_passes_when_inside_bounds() {
let validator = LengthValidator::new().in_range(3..=5);
let mut errors = Errors::new();
validator.validate("name", Some(&json!("Alice")), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn nil_value_fails_minimum_but_not_maximum() {
let mut minimum_errors = Errors::new();
let mut maximum_errors = Errors::new();
LengthValidator::new()
.minimum(1)
.validate("name", None, &mut minimum_errors);
LengthValidator::new()
.maximum(5)
.validate("name", None, &mut maximum_errors);
assert_eq!(minimum_errors.on("name")[0].error_type, ErrorType::TooShort);
assert!(maximum_errors.is_empty());
}
#[test]
fn counts_unicode_scalar_values() {
let validator = LengthValidator::new().is(5);
let mut errors = Errors::new();
validator.validate("title", Some(&json!("一二345å…")), &mut errors);
assert_eq!(
errors.on("title")[0].message,
"is the wrong length (should be 5 characters)"
);
}
#[test]
fn custom_messages_override_defaults() {
let validator = LengthValidator::new()
.minimum(5)
.too_short_message("need %{count}");
let mut errors = Errors::new();
validator.validate("password", Some(&json!("abc")), &mut errors);
assert_eq!(errors.on("password")[0].message, "need 5");
}
#[test]
fn uses_array_length_for_arrays() {
let validator = LengthValidator::new().is(2);
let mut errors = Errors::new();
validator.validate("tags", Some(&json!(["a"])), &mut errors);
assert_eq!(errors.on("tags")[0].error_type, ErrorType::WrongLength);
}
#[test]
fn minimum_passes_on_boundary() {
let errors = validate_length(LengthValidator::new().minimum(3), Some(json!("Cat")));
assert!(errors.is_empty());
}
#[test]
fn maximum_passes_on_boundary() {
let errors = validate_length(LengthValidator::new().maximum(3), Some(json!("Cat")));
assert!(errors.is_empty());
}
#[test]
fn exact_length_passes_when_equal() {
let errors = validate_length(LengthValidator::new().is(4), Some(json!("code")));
assert!(errors.is_empty());
}
#[test]
fn inclusive_range_passes_on_lower_boundary() {
let errors = validate_length(LengthValidator::new().in_range(2..=4), Some(json!("ab")));
assert!(errors.is_empty());
}
#[test]
fn inclusive_range_passes_on_upper_boundary() {
let errors = validate_length(LengthValidator::new().in_range(2..=4), Some(json!("abcd")));
assert!(errors.is_empty());
}
#[test]
fn wrong_length_message_override_is_used() {
let errors = validate_length(
LengthValidator::new()
.is(2)
.wrong_length_message("need %{count} chars"),
Some(json!("abc")),
);
assert_eq!(errors.on("field")[0].message, "need 2 chars");
}
#[test]
fn too_long_message_override_is_used() {
let errors = validate_length(
LengthValidator::new()
.maximum(2)
.too_long_message("max %{count}"),
Some(json!("abcd")),
);
assert_eq!(errors.on("field")[0].message, "max 2");
}
#[test]
fn generic_message_overrides_too_short_default() {
let errors = validate_length(
LengthValidator::new()
.minimum(5)
.message("minimum %{count}"),
Some(json!("abc")),
);
assert_eq!(errors.on("field")[0].message, "minimum 5");
}
#[test]
fn generic_message_overrides_too_long_default() {
let errors = validate_length(
LengthValidator::new()
.maximum(2)
.message("maximum %{count}"),
Some(json!("abcd")),
);
assert_eq!(errors.on("field")[0].message, "maximum 2");
}
#[test]
fn generic_message_overrides_wrong_length_default() {
let errors = validate_length(
LengthValidator::new().is(2).message("exactly %{count}"),
Some(json!("abcd")),
);
assert_eq!(errors.on("field")[0].message, "exactly 2");
}
#[test]
fn allow_nil_skips_missing_values_in_validation_set() {
let mut set = ValidationSet::new();
set.add("title", LengthValidator::new().minimum(1).allow_nil());
let mut errors = Errors::new();
let _ = set.validate(&|_| None, &mut errors);
assert!(errors.is_empty());
}
#[test]
fn allow_blank_skips_whitespace_strings_in_validation_set() {
let mut set = ValidationSet::new();
set.add("title", LengthValidator::new().minimum(2).allow_blank());
let mut errors = Errors::new();
let _ = set.validate(&|_| Some(json!(" ")), &mut errors);
assert!(errors.is_empty());
}
#[test]
fn empty_array_fails_minimum() {
let errors = validate_length(LengthValidator::new().minimum(1), Some(json!([])));
assert_eq!(errors.on("field")[0].error_type, ErrorType::TooShort);
}
#[test]
fn array_within_range_passes() {
let errors = validate_length(LengthValidator::new().in_range(1..=3), Some(json!([1, 2])));
assert!(errors.is_empty());
}
#[test]
fn object_length_counts_keys() {
let errors = validate_length(
LengthValidator::new().is(2),
Some(json!({ "first": 1, "second": 2, "third": 3 })),
);
assert_eq!(
errors.on("field")[0].message,
"is the wrong length (should be 2 characters)"
);
}
#[test]
fn bool_length_counts_rendered_characters() {
let errors = validate_length(LengthValidator::new().is(4), Some(json!(true)));
assert!(errors.is_empty());
}
#[test]
fn number_length_counts_rendered_digits() {
let errors = validate_length(LengthValidator::new().maximum(3), Some(json!(1234)));
assert_eq!(errors.on("field")[0].error_type, ErrorType::TooLong);
}
#[test]
fn exact_length_returns_before_minimum_and_maximum_checks() {
let errors = validate_length(
LengthValidator::new().is(2).minimum(5).maximum(1),
Some(json!("abcd")),
);
assert_eq!(errors.count(), 1);
assert_eq!(errors.on("field")[0].error_type, ErrorType::WrongLength);
}
#[test]
fn length_errors_include_count_details() {
let errors = validate_length(LengthValidator::new().minimum(3), Some(json!("a")));
assert_eq!(errors.details()[0].details.get("count"), Some(&json!(3)));
}
#[test]
fn full_message_humanizes_attribute_name() {
let mut errors = Errors::new();
LengthValidator::new().maximum(2).validate(
"line_items_count",
Some(&json!("abcd")),
&mut errors,
);
assert_eq!(
errors.full_messages(),
vec!["Line items count is too long (maximum is 2 characters)".to_string()],
);
}
}