Skip to main content

karbon_framework/validation/constraints/date/
date_range.rs

1use crate::validation::constraints::{Constraint, ConstraintResult, ConstraintViolation};
2use chrono::NaiveDate;
3
4/// Validates that a date falls within a given range.
5pub struct DateRange {
6    pub min: Option<NaiveDate>,
7    pub max: Option<NaiveDate>,
8    pub format: String,
9    pub min_message: String,
10    pub max_message: String,
11    pub invalid_message: String,
12}
13
14impl Default for DateRange {
15    fn default() -> Self {
16        Self {
17            min: None,
18            max: None,
19            format: "%Y-%m-%d".to_string(),
20            min_message: "This date should be {{ limit }} or later.".to_string(),
21            max_message: "This date should be {{ limit }} or earlier.".to_string(),
22            invalid_message: "This value is not a valid date.".to_string(),
23        }
24    }
25}
26
27impl DateRange {
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    pub fn min(mut self, date: NaiveDate) -> Self {
33        self.min = Some(date);
34        self
35    }
36
37    pub fn max(mut self, date: NaiveDate) -> Self {
38        self.max = Some(date);
39        self
40    }
41
42    pub fn between(min: NaiveDate, max: NaiveDate) -> Self {
43        Self {
44            min: Some(min),
45            max: Some(max),
46            ..Self::default()
47        }
48    }
49
50    pub fn with_format(mut self, format: impl Into<String>) -> Self {
51        self.format = format.into();
52        self
53    }
54
55    pub fn future_only() -> Self {
56        Self {
57            min: Some(chrono::Utc::now().date_naive()),
58            min_message: "This date should be in the future.".to_string(),
59            ..Self::default()
60        }
61    }
62
63    pub fn past_only() -> Self {
64        Self {
65            max: Some(chrono::Utc::now().date_naive()),
66            max_message: "This date should be in the past.".to_string(),
67            ..Self::default()
68        }
69    }
70}
71
72impl Constraint for DateRange {
73    fn validate(&self, value: &str) -> ConstraintResult {
74        let date = NaiveDate::parse_from_str(value, &self.format).map_err(|_| {
75            ConstraintViolation::new(self.name(), &self.invalid_message, value)
76        })?;
77
78        if let Some(min) = self.min {
79            if date < min {
80                return Err(ConstraintViolation::new(
81                    self.name(),
82                    self.min_message.replace("{{ limit }}", &min.to_string()),
83                    value,
84                ));
85            }
86        }
87
88        if let Some(max) = self.max {
89            if date > max {
90                return Err(ConstraintViolation::new(
91                    self.name(),
92                    self.max_message.replace("{{ limit }}", &max.to_string()),
93                    value,
94                ));
95            }
96        }
97
98        Ok(())
99    }
100
101    fn name(&self) -> &'static str {
102        "DateRange"
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_within_range() {
112        let constraint = DateRange::between(
113            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
114            NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
115        );
116        assert!(constraint.validate("2024-06-15").is_ok());
117        assert!(constraint.validate("2024-01-01").is_ok());
118        assert!(constraint.validate("2024-12-31").is_ok());
119    }
120
121    #[test]
122    fn test_out_of_range() {
123        let constraint = DateRange::between(
124            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
125            NaiveDate::from_ymd_opt(2024, 12, 31).unwrap(),
126        );
127        assert!(constraint.validate("2023-12-31").is_err());
128        assert!(constraint.validate("2025-01-01").is_err());
129    }
130
131    #[test]
132    fn test_min_only() {
133        let constraint = DateRange::new().min(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap());
134        assert!(constraint.validate("2024-06-15").is_ok());
135        assert!(constraint.validate("2023-12-31").is_err());
136    }
137
138    #[test]
139    fn test_max_only() {
140        let constraint = DateRange::new().max(NaiveDate::from_ymd_opt(2024, 12, 31).unwrap());
141        assert!(constraint.validate("2024-06-15").is_ok());
142        assert!(constraint.validate("2025-01-01").is_err());
143    }
144
145    #[test]
146    fn test_invalid_date_format() {
147        let constraint = DateRange::new();
148        assert!(constraint.validate("not-a-date").is_err());
149    }
150
151    #[test]
152    fn test_past_only() {
153        let constraint = DateRange::past_only();
154        assert!(constraint.validate("2020-01-01").is_ok());
155        assert!(constraint.validate("2099-01-01").is_err());
156    }
157
158    #[test]
159    fn test_future_only() {
160        let constraint = DateRange::future_only();
161        assert!(constraint.validate("2099-01-01").is_ok());
162        assert!(constraint.validate("2020-01-01").is_err());
163    }
164}