use crate::{Rule, RuleContext, ValidationError};
use chrono::{DateTime, Datelike, NaiveDate, Utc};
pub fn past() -> Rule<DateTime<Utc>> {
Rule::new(|value: &DateTime<Utc>, ctx: &RuleContext| {
let now = Utc::now();
if *value < now {
ValidationError::default()
} else {
ValidationError::single(ctx.full_path(), "not_in_past", "Must be in the past")
}
})
}
pub fn future() -> Rule<DateTime<Utc>> {
Rule::new(|value: &DateTime<Utc>, ctx: &RuleContext| {
let now = Utc::now();
if *value > now {
ValidationError::default()
} else {
ValidationError::single(ctx.full_path(), "not_in_future", "Must be in the future")
}
})
}
pub fn before(limit: DateTime<Utc>) -> Rule<DateTime<Utc>> {
Rule::new(move |value: &DateTime<Utc>, ctx: &RuleContext| {
if *value < limit {
ValidationError::default()
} else {
let mut err = ValidationError::single(
ctx.full_path(),
"not_before",
format!("Must be before {}", limit.to_rfc3339()),
);
err.violations[0].meta.insert("limit", limit.to_rfc3339());
err
}
})
}
pub fn after(limit: DateTime<Utc>) -> Rule<DateTime<Utc>> {
Rule::new(move |value: &DateTime<Utc>, ctx: &RuleContext| {
if *value > limit {
ValidationError::default()
} else {
let mut err = ValidationError::single(
ctx.full_path(),
"not_after",
format!("Must be after {}", limit.to_rfc3339()),
);
err.violations[0].meta.insert("limit", limit.to_rfc3339());
err
}
})
}
pub fn age_range(min: u32, max: u32) -> Rule<NaiveDate> {
Rule::new(move |birth_date: &NaiveDate, ctx: &RuleContext| {
let today = Utc::now().date_naive();
match calculate_age(*birth_date, today) {
None => {
let mut err = ValidationError::single(
ctx.full_path(),
"invalid_birth_date",
"Birth date cannot be in the future",
);
err.violations[0]
.meta
.insert("birth_date", birth_date.to_string());
err
}
Some(age) if age >= min && age <= max => ValidationError::default(),
Some(age) => {
let mut err = ValidationError::single(
ctx.full_path(),
"age_out_of_range",
format!("Age must be between {} and {} years", min, max),
);
err.violations[0].meta.insert("min", min.to_string());
err.violations[0].meta.insert("max", max.to_string());
err.violations[0].meta.insert("age", age.to_string());
err
}
}
})
}
fn calculate_age(birth_date: NaiveDate, current_date: NaiveDate) -> Option<u32> {
if birth_date > current_date {
return None;
}
let mut age = current_date.year() - birth_date.year();
if current_date.month() < birth_date.month()
|| (current_date.month() == birth_date.month() && current_date.day() < birth_date.day())
{
age -= 1;
}
Some(age as u32)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[test]
fn test_past_valid() {
let rule = past();
let yesterday = Utc::now() - Duration::days(1);
assert!(rule.apply(&yesterday).is_empty());
let last_year = Utc::now() - Duration::days(365);
assert!(rule.apply(&last_year).is_empty());
}
#[test]
fn test_past_invalid() {
let rule = past();
let tomorrow = Utc::now() + Duration::days(1);
let result = rule.apply(&tomorrow);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "not_in_past");
}
#[test]
fn test_future_valid() {
let rule = future();
let tomorrow = Utc::now() + Duration::days(1);
assert!(rule.apply(&tomorrow).is_empty());
let next_year = Utc::now() + Duration::days(365);
assert!(rule.apply(&next_year).is_empty());
}
#[test]
fn test_future_invalid() {
let rule = future();
let yesterday = Utc::now() - Duration::days(1);
let result = rule.apply(&yesterday);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "not_in_future");
}
#[test]
fn test_before_valid() {
let limit = Utc::now() + Duration::days(30);
let rule = before(limit);
let today = Utc::now();
assert!(rule.apply(&today).is_empty());
let tomorrow = Utc::now() + Duration::days(1);
assert!(rule.apply(&tomorrow).is_empty());
}
#[test]
fn test_before_invalid() {
let limit = Utc::now();
let rule = before(limit);
let tomorrow = Utc::now() + Duration::days(1);
let result = rule.apply(&tomorrow);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "not_before");
assert!(result.violations[0].meta.get("limit").is_some());
}
#[test]
fn test_after_valid() {
let limit = Utc::now() - Duration::days(30);
let rule = after(limit);
let today = Utc::now();
assert!(rule.apply(&today).is_empty());
let tomorrow = Utc::now() + Duration::days(1);
assert!(rule.apply(&tomorrow).is_empty());
}
#[test]
fn test_after_invalid() {
let limit = Utc::now();
let rule = after(limit);
let yesterday = Utc::now() - Duration::days(1);
let result = rule.apply(&yesterday);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "not_after");
assert!(result.violations[0].meta.get("limit").is_some());
}
#[test]
fn test_age_range_valid() {
let rule = age_range(18, 120);
let birth_date = NaiveDate::from_ymd_opt(Utc::now().year() - 25, 6, 15).unwrap();
assert!(rule.apply(&birth_date).is_empty());
let birth_date = NaiveDate::from_ymd_opt(Utc::now().year() - 18, 1, 1).unwrap();
assert!(rule.apply(&birth_date).is_empty());
}
#[test]
fn test_age_range_too_young() {
let rule = age_range(18, 120);
let birth_date = NaiveDate::from_ymd_opt(Utc::now().year() - 10, 1, 1).unwrap();
let result = rule.apply(&birth_date);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "age_out_of_range");
assert_eq!(result.violations[0].meta.get("min"), Some("18"));
let age = result.violations[0].meta.get("age").unwrap();
assert!(
age == "9" || age == "10",
"Expected age 9 or 10, got {}",
age
);
}
#[test]
fn test_age_range_too_old() {
let rule = age_range(18, 120);
let birth_date = NaiveDate::from_ymd_opt(Utc::now().year() - 130, 1, 1).unwrap();
let result = rule.apply(&birth_date);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "age_out_of_range");
assert_eq!(result.violations[0].meta.get("max"), Some("120"));
let age = result.violations[0].meta.get("age").unwrap();
assert!(
age == "129" || age == "130",
"Expected age 129 or 130, got {}",
age
);
}
#[test]
fn test_age_range_future_birth_date() {
let rule = age_range(18, 120);
let birth_date = NaiveDate::from_ymd_opt(Utc::now().year() + 5, 6, 15).unwrap();
let result = rule.apply(&birth_date);
assert!(!result.is_empty());
assert_eq!(result.violations[0].code, "invalid_birth_date");
assert!(result.violations[0].meta.get("birth_date").is_some());
}
#[test]
fn test_calculate_age() {
let birth_date = NaiveDate::from_ymd_opt(2000, 6, 15).unwrap();
let current = NaiveDate::from_ymd_opt(2025, 3, 10).unwrap();
assert_eq!(calculate_age(birth_date, current), Some(24));
let current = NaiveDate::from_ymd_opt(2025, 8, 20).unwrap();
assert_eq!(calculate_age(birth_date, current), Some(25));
let current = NaiveDate::from_ymd_opt(2025, 6, 15).unwrap();
assert_eq!(calculate_age(birth_date, current), Some(25));
let future_birth = NaiveDate::from_ymd_opt(2030, 1, 1).unwrap();
let current = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
assert_eq!(calculate_age(future_birth, current), None);
}
#[test]
fn test_before_after_composition() {
let start = Utc::now();
let end = Utc::now() + Duration::days(30);
let rule = after(start).and(before(end));
let event = Utc::now() + Duration::days(15);
assert!(rule.apply(&event).is_empty());
let event = Utc::now() - Duration::days(1);
let result = rule.apply(&event);
assert!(!result.is_empty());
let event = Utc::now() + Duration::days(31);
let result = rule.apply(&event);
assert!(!result.is_empty());
}
}