use crate::{Rule, RuleContext, ValidationError};
pub fn range<T>(min: T, max: T) -> Rule<T>
where
T: PartialOrd + Copy + std::fmt::Display + Send + Sync + 'static,
{
Rule::new(move |value: &T, ctx: &RuleContext| {
if *value < min || *value > max {
let mut err = ValidationError::single(
ctx.full_path(),
"out_of_range",
format!("Must be between {} and {}", min, max),
);
err.violations[0].meta.insert("min", min.to_string());
err.violations[0].meta.insert("max", max.to_string());
err
} else {
ValidationError::default()
}
})
}
pub fn min<T>(min: T) -> Rule<T>
where
T: PartialOrd + Copy + std::fmt::Display + Send + Sync + 'static,
{
Rule::new(move |value: &T, ctx: &RuleContext| {
if *value < min {
let mut err = ValidationError::single(
ctx.full_path(),
"below_minimum",
format!("Must be at least {}", min),
);
err.violations[0].meta.insert("min", min.to_string());
err
} else {
ValidationError::default()
}
})
}
pub fn max<T>(max: T) -> Rule<T>
where
T: PartialOrd + Copy + std::fmt::Display + Send + Sync + 'static,
{
Rule::new(move |value: &T, ctx: &RuleContext| {
if *value > max {
let mut err = ValidationError::single(
ctx.full_path(),
"above_maximum",
format!("Must be at most {}", max),
);
err.violations[0].meta.insert("max", max.to_string());
err
} else {
ValidationError::default()
}
})
}
pub fn non_zero<T>() -> Rule<T>
where
T: PartialEq + Default + Copy + Send + Sync + 'static,
{
Rule::new(move |value: &T, ctx: &RuleContext| {
if *value != T::default() {
ValidationError::default()
} else {
ValidationError::single(ctx.full_path(), "zero_value", "Must be non-zero")
}
})
}
pub fn positive<T>() -> Rule<T>
where
T: PartialOrd + Default + Copy + Send + Sync + 'static,
{
Rule::new(move |value: &T, ctx: &RuleContext| {
if *value > T::default() {
ValidationError::default()
} else {
ValidationError::single(
ctx.full_path(),
"not_positive",
"Must be positive (greater than zero)",
)
}
})
}
pub fn negative<T>() -> Rule<T>
where
T: PartialOrd + Default + Copy + Send + Sync + 'static,
{
Rule::new(move |value: &T, ctx: &RuleContext| {
if *value < T::default() {
ValidationError::default()
} else {
ValidationError::single(
ctx.full_path(),
"not_negative",
"Must be negative (less than zero)",
)
}
})
}
pub fn finite<T>() -> Rule<T>
where
T: FiniteCheck + Copy + Send + Sync + 'static,
{
Rule::new(move |value: &T, ctx: &RuleContext| {
if value.is_finite_value() {
ValidationError::default()
} else {
ValidationError::single(
ctx.full_path(),
"not_finite",
"Must be a finite number (not NaN or infinity)",
)
}
})
}
pub trait FiniteCheck {
fn is_finite_value(&self) -> bool;
}
impl FiniteCheck for f32 {
fn is_finite_value(&self) -> bool {
self.is_finite()
}
}
impl FiniteCheck for f64 {
fn is_finite_value(&self) -> bool {
self.is_finite()
}
}
pub fn float_range<T>(min: T, max: T) -> Rule<T>
where
T: PartialOrd + Copy + std::fmt::Display + Send + Sync + 'static + FiniteCheck,
{
finite().and(range(min, max))
}
pub fn float_min<T>(minimum: T) -> Rule<T>
where
T: PartialOrd + Copy + std::fmt::Display + Send + Sync + 'static + FiniteCheck,
{
finite().and(min(minimum))
}
pub fn float_max<T>(maximum: T) -> Rule<T>
where
T: PartialOrd + Copy + std::fmt::Display + Send + Sync + 'static + FiniteCheck,
{
finite().and(max(maximum))
}
pub fn multiple_of<T>(divisor: T) -> Rule<T>
where
T: std::ops::Rem<Output = T>
+ PartialEq
+ Default
+ Copy
+ std::fmt::Display
+ Send
+ Sync
+ 'static,
{
assert!(
divisor != T::default(),
"multiple_of: divisor cannot be zero"
);
Rule::new(move |value: &T, ctx: &RuleContext| {
if *value % divisor == T::default() {
ValidationError::default()
} else {
let mut err = ValidationError::single(
ctx.full_path(),
"not_multiple",
format!("Must be a multiple of {}", divisor),
);
err.violations[0]
.meta
.insert("divisor", divisor.to_string());
err
}
})
}
pub fn try_multiple_of<T>(divisor: T) -> Option<Rule<T>>
where
T: std::ops::Rem<Output = T>
+ PartialEq
+ Default
+ Copy
+ std::fmt::Display
+ Send
+ Sync
+ 'static,
{
if divisor == T::default() {
return None;
}
Some(Rule::new(move |value: &T, ctx: &RuleContext| {
if *value % divisor == T::default() {
ValidationError::default()
} else {
let mut err = ValidationError::single(
ctx.full_path(),
"not_multiple",
format!("Must be a multiple of {}", divisor),
);
err.violations[0]
.meta
.insert("divisor", divisor.to_string());
err
}
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_range_u8_valid() {
let rule = range(18u8, 120u8);
assert!(rule.apply(&18).is_empty());
assert!(rule.apply(&50).is_empty());
assert!(rule.apply(&120).is_empty());
}
#[test]
fn test_range_u8_below() {
let rule = range(18u8, 120u8);
let result = rule.apply(&17);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "out_of_range");
assert_eq!(result.violations[0].meta.get("min"), Some("18"));
assert_eq!(result.violations[0].meta.get("max"), Some("120"));
}
#[test]
fn test_range_u8_above() {
let rule = range(18u8, 120u8);
let result = rule.apply(&121);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "out_of_range");
}
#[test]
fn test_range_i32() {
let rule = range(-10i32, 10i32);
assert!(rule.apply(&0).is_empty());
assert!(rule.apply(&-10).is_empty());
assert!(rule.apply(&10).is_empty());
let result = rule.apply(&-11);
assert!(!result.is_empty());
let result = rule.apply(&11);
assert!(!result.is_empty());
}
#[test]
fn test_range_f64() {
let rule = range(0.0f64, 1.0f64);
assert!(rule.apply(&0.5).is_empty());
assert!(rule.apply(&0.0).is_empty());
assert!(rule.apply(&1.0).is_empty());
let result = rule.apply(&-0.1);
assert!(!result.is_empty());
let result = rule.apply(&1.1);
assert!(!result.is_empty());
}
#[test]
fn test_min_valid() {
let rule = min(18u8);
assert!(rule.apply(&18).is_empty());
assert!(rule.apply(&100).is_empty());
}
#[test]
fn test_min_invalid() {
let rule = min(18u8);
let result = rule.apply(&17);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "below_minimum");
assert_eq!(result.violations[0].meta.get("min"), Some("18"));
}
#[test]
fn test_max_valid() {
let rule = max(100u8);
assert!(rule.apply(&100).is_empty());
assert!(rule.apply(&50).is_empty());
}
#[test]
fn test_max_invalid() {
let rule = max(100u8);
let result = rule.apply(&101);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "above_maximum");
assert_eq!(result.violations[0].meta.get("max"), Some("100"));
}
#[test]
fn test_min_and_max_composition() {
let rule = min(18u8).and(max(120u8));
assert!(rule.apply(&50).is_empty());
let result = rule.apply(&17);
assert_eq!(result.violations.len(), 1);
assert_eq!(result.violations[0].code, "below_minimum");
let result = rule.apply(&121);
assert_eq!(result.violations.len(), 1);
assert_eq!(result.violations[0].code, "above_maximum");
}
#[test]
fn test_positive_valid() {
let rule = positive();
assert!(rule.apply(&1i32).is_empty());
assert!(rule.apply(&100i32).is_empty());
}
#[test]
fn test_positive_invalid() {
let rule = positive();
let result = rule.apply(&0i32);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "not_positive");
let result = rule.apply(&-1i32);
assert!(!result.is_empty());
}
#[test]
fn test_negative_valid() {
let rule = negative();
assert!(rule.apply(&-1i32).is_empty());
assert!(rule.apply(&-100i32).is_empty());
}
#[test]
fn test_negative_invalid() {
let rule = negative();
let result = rule.apply(&0i32);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "not_negative");
let result = rule.apply(&1i32);
assert!(!result.is_empty());
}
#[test]
fn test_multiple_of_valid() {
let rule = multiple_of(5);
assert!(rule.apply(&10).is_empty());
assert!(rule.apply(&15).is_empty());
assert!(rule.apply(&0).is_empty());
assert!(rule.apply(&-10).is_empty());
}
#[test]
fn test_multiple_of_invalid() {
let rule = multiple_of(5);
let result = rule.apply(&7);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "not_multiple");
assert_eq!(result.violations[0].meta.get("divisor"), Some("5"));
let result = rule.apply(&3);
assert!(!result.is_empty());
}
#[test]
fn test_non_zero_valid_signed() {
let rule = non_zero();
assert!(rule.apply(&1i32).is_empty());
assert!(rule.apply(&-1i32).is_empty());
assert!(rule.apply(&100i32).is_empty());
}
#[test]
fn test_non_zero_valid_unsigned() {
let rule = non_zero();
assert!(rule.apply(&1u8).is_empty());
assert!(rule.apply(&100u8).is_empty());
}
#[test]
fn test_non_zero_invalid() {
let rule = non_zero();
let result = rule.apply(&0i32);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "zero_value");
}
#[test]
fn test_non_zero_invalid_unsigned() {
let rule = non_zero();
let result = rule.apply(&0u8);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "zero_value");
}
#[test]
fn test_finite_f64_valid() {
let rule = finite();
assert!(rule.apply(&0.0f64).is_empty());
assert!(rule.apply(&1.5).is_empty());
assert!(rule.apply(&-100.5).is_empty());
assert!(rule.apply(&f64::MIN).is_empty());
assert!(rule.apply(&f64::MAX).is_empty());
}
#[test]
fn test_finite_f64_invalid() {
let rule = finite();
let result = rule.apply(&f64::NAN);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "not_finite");
let result = rule.apply(&f64::INFINITY);
assert!(!result.is_empty());
let result = rule.apply(&f64::NEG_INFINITY);
assert!(!result.is_empty());
}
#[test]
fn test_finite_f32_valid() {
let rule: Rule<f32> = finite();
assert!(rule.apply(&0.0f32).is_empty());
assert!(rule.apply(&1.5f32).is_empty());
assert!(rule.apply(&-100.5f32).is_empty());
}
#[test]
fn test_finite_f32_invalid() {
let rule: Rule<f32> = finite();
let result = rule.apply(&f32::NAN);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "not_finite");
let result = rule.apply(&f32::INFINITY);
assert!(!result.is_empty());
}
#[test]
fn test_finite_with_range() {
let rule = finite().and(range(0.0, 1.0));
assert!(rule.apply(&0.5).is_empty());
assert!(rule.apply(&0.0).is_empty());
assert!(rule.apply(&1.0).is_empty());
let result = rule.apply(&f64::NAN);
assert_eq!(result.violations.len(), 1);
assert_eq!(result.violations[0].code, "not_finite");
let result = rule.apply(&1.5);
assert_eq!(result.violations.len(), 1);
assert_eq!(result.violations[0].code, "out_of_range");
}
#[test]
fn test_float_range() {
let rule = float_range(0.0f64, 1.0);
assert!(rule.apply(&0.5).is_empty());
assert!(rule.apply(&0.0).is_empty());
assert!(rule.apply(&1.0).is_empty());
assert!(!rule.apply(&1.5).is_empty());
assert!(!rule.apply(&-0.1).is_empty());
assert!(!rule.apply(&f64::NAN).is_empty());
assert!(!rule.apply(&f64::INFINITY).is_empty());
assert!(!rule.apply(&f64::NEG_INFINITY).is_empty());
}
#[test]
fn test_float_min() {
let rule = float_min(0.0f64);
assert!(rule.apply(&0.0).is_empty());
assert!(rule.apply(&100.0).is_empty());
assert!(!rule.apply(&-1.0).is_empty());
assert!(!rule.apply(&f64::NAN).is_empty());
assert!(!rule.apply(&f64::NEG_INFINITY).is_empty());
}
#[test]
fn test_float_max() {
let rule = float_max(100.0f64);
assert!(rule.apply(&50.0).is_empty());
assert!(rule.apply(&100.0).is_empty());
assert!(!rule.apply(&101.0).is_empty());
assert!(!rule.apply(&f64::NAN).is_empty());
assert!(!rule.apply(&f64::INFINITY).is_empty());
}
#[test]
fn test_try_multiple_of_valid() {
let rule = try_multiple_of(5).unwrap();
assert!(rule.apply(&10).is_empty());
assert!(rule.apply(&15).is_empty());
assert!(!rule.apply(&7).is_empty());
}
#[test]
fn test_try_multiple_of_zero_divisor() {
let result = try_multiple_of(0);
assert!(result.is_none());
}
#[test]
#[should_panic(expected = "multiple_of: divisor cannot be zero")]
fn test_multiple_of_zero_divisor_panics() {
let _rule = multiple_of(0);
}
#[test]
fn test_range_min_equals_max() {
let rule = range(5i32, 5i32);
assert!(rule.apply(&5).is_empty());
assert!(!rule.apply(&4).is_empty());
assert!(!rule.apply(&6).is_empty());
}
#[test]
fn test_range_zero_crossing() {
let rule = range(-10i32, 10i32);
assert!(rule.apply(&0).is_empty());
assert!(rule.apply(&-10).is_empty());
assert!(rule.apply(&10).is_empty());
assert!(!rule.apply(&-11).is_empty());
assert!(!rule.apply(&11).is_empty());
}
#[test]
fn test_finite_min_positive() {
let rule: Rule<f64> = finite();
assert!(rule.apply(&f64::MIN_POSITIVE).is_empty());
}
#[test]
fn test_float_range_very_small_decimals() {
let rule = float_range(0.0f64, 0.001f64);
assert!(rule.apply(&0.0005).is_empty());
assert!(rule.apply(&0.0).is_empty());
assert!(rule.apply(&0.001).is_empty());
assert!(!rule.apply(&0.002).is_empty());
}
#[test]
fn test_float_min_with_negative() {
let rule = float_min(-100.5f64);
assert!(rule.apply(&-100.5).is_empty());
assert!(rule.apply(&0.0).is_empty());
assert!(!rule.apply(&-100.6).is_empty());
}
#[test]
fn test_float_max_with_negative() {
let rule = float_max(-0.1f64);
assert!(rule.apply(&-0.1).is_empty());
assert!(rule.apply(&-100.0).is_empty());
assert!(!rule.apply(&0.0).is_empty());
}
#[test]
fn test_negative_infinity_specific() {
let rule: Rule<f64> = finite();
let result = rule.apply(&f64::NEG_INFINITY);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "not_finite");
}
#[test]
fn test_multiple_of_negative_divisor() {
let rule = multiple_of(-5);
assert!(rule.apply(&10).is_empty());
assert!(rule.apply(&-10).is_empty());
assert!(rule.apply(&0).is_empty());
assert!(!rule.apply(&7).is_empty());
}
#[test]
fn test_multiple_of_one() {
let rule = multiple_of(1);
assert!(rule.apply(&0).is_empty());
assert!(rule.apply(&1).is_empty());
assert!(rule.apply(&-1).is_empty());
assert!(rule.apply(&1000).is_empty());
}
#[test]
fn test_range_f64_precision() {
let rule = range(0.0f64, 1.0f64);
let sum = 0.1 + 0.2;
assert!(rule.apply(&sum).is_empty());
}
#[test]
fn test_min_max_message_format() {
let rule = min(10i32);
let result = rule.apply(&5);
assert!(result.violations[0].message.contains("10"));
let rule = max(10i32);
let result = rule.apply(&15);
assert!(result.violations[0].message.contains("10"));
}
#[test]
fn test_range_message_format() {
let rule = range(10i32, 20i32);
let result = rule.apply(&5);
assert!(result.violations[0].message.contains("10"));
assert!(result.violations[0].message.contains("20"));
}
#[test]
fn test_positive_negative_with_float() {
let rule_pos: Rule<f64> = positive();
let rule_neg: Rule<f64> = negative();
assert!(rule_pos.apply(&0.001).is_empty());
assert!(!rule_pos.apply(&-0.001).is_empty());
assert!(!rule_pos.apply(&0.0).is_empty());
assert!(rule_neg.apply(&-0.001).is_empty());
assert!(!rule_neg.apply(&0.001).is_empty());
assert!(!rule_neg.apply(&0.0).is_empty());
}
#[test]
fn test_non_zero_with_float() {
let rule: Rule<f64> = non_zero();
assert!(rule.apply(&0.001).is_empty());
assert!(rule.apply(&-0.001).is_empty());
let result = rule.apply(&0.0);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "zero_value");
}
#[test]
fn test_float_range_f32() {
let rule: Rule<f32> = float_range(0.0f32, 1.0f32);
assert!(rule.apply(&0.5f32).is_empty());
assert!(!rule.apply(&f32::NAN).is_empty());
assert!(!rule.apply(&f32::INFINITY).is_empty());
}
}