fraiseql_core/validation/
date_validators.rs1use std::cmp::Ordering;
24
25use chrono::Datelike;
26
27use crate::error::{FraiseQLError, Result};
28
29pub(crate) fn parse_date(date_str: &str) -> Result<(u32, u32, u32)> {
31 let parts: Vec<&str> = date_str.split('-').collect();
32 if parts.len() != 3 {
33 return Err(FraiseQLError::Validation {
34 message: format!("Invalid date format: '{}'. Expected YYYY-MM-DD", date_str),
35 path: None,
36 });
37 }
38
39 let year = parts[0].parse::<u32>().map_err(|_| FraiseQLError::Validation {
40 message: format!("Invalid year: '{}'", parts[0]),
41 path: None,
42 })?;
43
44 let month = parts[1].parse::<u32>().map_err(|_| FraiseQLError::Validation {
45 message: format!("Invalid month: '{}'", parts[1]),
46 path: None,
47 })?;
48
49 let day = parts[2].parse::<u32>().map_err(|_| FraiseQLError::Validation {
50 message: format!("Invalid day: '{}'", parts[2]),
51 path: None,
52 })?;
53
54 if !(1..=12).contains(&month) {
55 return Err(FraiseQLError::Validation {
56 message: format!("Month must be between 1 and 12, got {}", month),
57 path: None,
58 });
59 }
60
61 let days_in_month = get_days_in_month(month, year);
62 if !(1..=days_in_month).contains(&day) {
63 return Err(FraiseQLError::Validation {
64 message: format!("Day must be between 1 and {}, got {}", days_in_month, day),
65 path: None,
66 });
67 }
68
69 Ok((year, month, day))
70}
71
72pub(crate) const fn is_leap_year(year: u32) -> bool {
74 (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400)
75}
76
77pub(crate) const fn get_days_in_month(month: u32, year: u32) -> u32 {
79 match month {
80 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
81 4 | 6 | 9 | 11 => 30,
82 2 => {
83 if is_leap_year(year) {
84 29
85 } else {
86 28
87 }
88 },
89 _ => 0,
90 }
91}
92
93fn get_today() -> (u32, u32, u32) {
95 let today = chrono::Utc::now().date_naive();
96 (today.year_ce().1, today.month(), today.day())
97}
98
99pub(crate) fn compare_dates(left: (u32, u32, u32), right: (u32, u32, u32)) -> i32 {
101 match left.0.cmp(&right.0) {
102 Ordering::Less => -1,
103 Ordering::Greater => 1,
104 Ordering::Equal => match left.1.cmp(&right.1) {
105 Ordering::Less => -1,
106 Ordering::Greater => 1,
107 Ordering::Equal => match left.2.cmp(&right.2) {
108 Ordering::Less => -1,
109 Ordering::Greater => 1,
110 Ordering::Equal => 0,
111 },
112 },
113 }
114}
115
116pub(crate) fn days_between(left: (u32, u32, u32), right: (u32, u32, u32)) -> i64 {
118 let days_left = i64::from(left.0) * 365 + i64::from(left.1) * 31 + i64::from(left.2);
120 let days_right = i64::from(right.0) * 365 + i64::from(right.1) * 31 + i64::from(right.2);
121 days_left - days_right
122}
123
124pub fn validate_min_date(date_str: &str, min_date_str: &str) -> Result<()> {
131 let date = parse_date(date_str)?;
132 let min_date = parse_date(min_date_str)?;
133
134 if compare_dates(date, min_date) < 0 {
135 return Err(FraiseQLError::Validation {
136 message: format!("Date '{}' must be >= '{}'", date_str, min_date_str),
137 path: None,
138 });
139 }
140
141 Ok(())
142}
143
144pub fn validate_max_date(date_str: &str, max_date_str: &str) -> Result<()> {
151 let date = parse_date(date_str)?;
152 let max_date = parse_date(max_date_str)?;
153
154 if compare_dates(date, max_date) > 0 {
155 return Err(FraiseQLError::Validation {
156 message: format!("Date '{}' must be <= '{}'", date_str, max_date_str),
157 path: None,
158 });
159 }
160
161 Ok(())
162}
163
164pub fn validate_date_range(date_str: &str, min_date_str: &str, max_date_str: &str) -> Result<()> {
171 validate_min_date(date_str, min_date_str)?;
172 validate_max_date(date_str, max_date_str)?;
173 Ok(())
174}
175
176pub fn validate_min_age(date_str: &str, min_age: u32) -> Result<()> {
183 let birth_date = parse_date(date_str)?;
184 let today = get_today();
185
186 let mut age = today.0 - birth_date.0;
188 if (today.1, today.2) < (birth_date.1, birth_date.2) {
189 age -= 1;
190 }
191
192 if age < min_age {
193 return Err(FraiseQLError::Validation {
194 message: format!("Age must be at least {} years old, got {}", min_age, age),
195 path: None,
196 });
197 }
198
199 Ok(())
200}
201
202pub fn validate_max_age(date_str: &str, max_age: u32) -> Result<()> {
209 let birth_date = parse_date(date_str)?;
210 let today = get_today();
211
212 let mut age = today.0 - birth_date.0;
214 if (today.1, today.2) < (birth_date.1, birth_date.2) {
215 age -= 1;
216 }
217
218 if age > max_age {
219 return Err(FraiseQLError::Validation {
220 message: format!("Age must be at most {} years old, got {}", max_age, age),
221 path: None,
222 });
223 }
224
225 Ok(())
226}
227
228pub fn validate_max_days_in_future(date_str: &str, max_days: i64) -> Result<()> {
235 let date = parse_date(date_str)?;
236 let today = get_today();
237
238 let days_diff = days_between(date, today);
239 if days_diff > max_days {
240 return Err(FraiseQLError::Validation {
241 message: format!(
242 "Date '{}' cannot be more than {} days in the future",
243 date_str, max_days
244 ),
245 path: None,
246 });
247 }
248
249 Ok(())
250}
251
252pub fn validate_max_days_in_past(date_str: &str, max_days: i64) -> Result<()> {
259 let date = parse_date(date_str)?;
260 let today = get_today();
261
262 let days_diff = days_between(today, date);
263 if days_diff > max_days {
264 return Err(FraiseQLError::Validation {
265 message: format!(
266 "Date '{}' cannot be more than {} days in the past",
267 date_str, max_days
268 ),
269 path: None,
270 });
271 }
272
273 Ok(())
274}