Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/validation/rules.rs

19.2 KiB, 70 runs

created by r1870400018:6403, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Validation rules, and a library of ready-made ones.
2//!
3//! A rule is a name and up to three closures, one each for a CalClock, a
4//! CalendarDate and a ClockTime. Rules combine with all_rules and any_rule.
5//!
6//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
7//! Anthropic Claude
8
9use crate::{
10 calendar::{Calendar, CalendarDate},
11 clock::ClockTime,
12 time::CalClock,
13 validation::{ValidationError, ValidationResult},
14};
15
16use oxedyne_fe2o3_core::prelude::*;
17
18use std::{
19 collections::HashSet,
20 time::{SystemTime, UNIX_EPOCH},
21};
22
23/// # Examples
24///
25/// ```ignore
26/// use oxedyne_fe2o3_datime::{
27/// validation::{ValidationRule, CalClockValidator},
28/// time::CalClock,
29/// }res!();
30///
31/// // Create a rule that only allows business hours
32/// let business_hours_rule = ValidationRule::new("business_hours")
33/// .with_time_validator(|time| {
34/// let hour = time.hour().of()res!();
35/// if hour >= 9 && hour < 17 {
36/// Ok(())
37/// } else {
38/// Err(vec![ValidationError::new(
39/// "business_hours",
40/// "Time must be during business hours (9 AM - 5 PM)"
41/// )])
42/// }
43/// })res!();
44///
45/// let mut validator = CalClockValidator::new()res!();
46/// validator.add_rule(business_hours_rule)res!();
47/// ```
48pub struct ValidationRule {
49 name: String,
50 description: Option<String>,
51 // A rule may carry any combination of the three, and only the ones set
52 // are consulted.
53 calclock_validator: Option<Box<dyn Fn(&CalClock) -> ValidationResult + Send + Sync>>,
54 date_validator: Option<Box<dyn Fn(&CalendarDate) -> ValidationResult + Send + Sync>>,
55 time_validator: Option<Box<dyn Fn(&ClockTime) -> ValidationResult + Send + Sync>>,
56}
57
58impl std::fmt::Debug for ValidationRule {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.debug_struct("ValidationRule")
61 .field("name", &self.name)
62 .field("description", &self.description)
63 .field("has_calclock_validator", &self.calclock_validator.is_some())
64 .field("has_date_validator", &self.date_validator.is_some())
65 .field("has_time_validator", &self.time_validator.is_some())
66 .finish()
67 }
68}
69
70impl ValidationRule {
71 pub fn new<S: Into<String>>(name: S) -> Self {
72 Self {
73 name: name.into(),
74 description: None,
75 calclock_validator: None,
76 date_validator: None,
77 time_validator: None,
78 }
79 }
80
81 pub fn description<S: Into<String>>(mut self, description: S) -> Self {
82 self.description = Some(description.into());
83 self
84 }
85
86 pub fn with_calclock_validator<F>(mut self, validator: F) -> Self
87 where
88 F: Fn(&CalClock) -> ValidationResult + Send + Sync + 'static,
89 {
90 self.calclock_validator = Some(Box::new(validator));
91 self
92 }
93
94 pub fn with_date_validator<F>(mut self, validator: F) -> Self
95 where
96 F: Fn(&CalendarDate) -> ValidationResult + Send + Sync + 'static,
97 {
98 self.date_validator = Some(Box::new(validator));
99 self
100 }
101
102 pub fn with_time_validator<F>(mut self, validator: F) -> Self
103 where
104 F: Fn(&ClockTime) -> ValidationResult + Send + Sync + 'static,
105 {
106 self.time_validator = Some(Box::new(validator));
107 self
108 }
109
110 pub fn name(&self) -> &str {
111 &self.name
112 }
113
114 pub fn get_description(&self) -> Option<&str> {
115 self.description.as_deref()
116 }
117
118 pub fn validate_calclock(&self, calclock: &CalClock) -> ValidationResult {
119 let mut errors = Vec::new();
120
121 // Check calclock-specific validator
122 if let Some(ref validator) = self.calclock_validator {
123 if let Err(mut errs) = validator(calclock) {
124 errors.append(&mut errs);
125 }
126 }
127
128 // Also check date validator on the date component
129 if let Some(ref validator) = self.date_validator {
130 if let Err(mut errs) = validator(&calclock.date()) {
131 errors.append(&mut errs);
132 }
133 }
134
135 // Also check time validator on the time component
136 if let Some(ref validator) = self.time_validator {
137 if let Err(mut errs) = validator(&calclock.time()) {
138 errors.append(&mut errs);
139 }
140 }
141
142 if errors.is_empty() {
143 Ok(())
144 } else {
145 Err(errors)
146 }
147 }
148
149 pub fn validate_date(&self, date: &CalendarDate) -> ValidationResult {
150 if let Some(ref validator) = self.date_validator {
151 validator(date)
152 } else {
153 Ok(())
154 }
155 }
156
157 pub fn validate_time(&self, time: &ClockTime) -> ValidationResult {
158 if let Some(ref validator) = self.time_validator {
159 validator(time)
160 } else {
161 Ok(())
162 }
163 }
164}
165
166pub struct ValidationRules;
167
168impl ValidationRules {
169 /// 9 AM to 5 PM, Monday to Friday.
170 pub fn business_hours() -> ValidationRule {
171 ValidationRule::new("business_hours")
172 .description("Only allows times during standard business hours (9 AM - 5 PM, Monday-Friday)")
173 .with_calclock_validator(|calclock| {
174 // Check if it's a weekday
175 if !calclock.date().is_business_day() {
176 return Err(vec![ValidationError::new(
177 "business_hours",
178 "Date must be a business day (Monday-Friday)"
179 )]);
180 }
181
182 // Check if time is within business hours
183 let hour = calclock.time().hour().of();
184 if hour < 9 || hour >= 17 {
185 return Err(vec![ValidationError::new(
186 "business_hours",
187 "Time must be during business hours (9 AM - 5 PM)"
188 ).field("hour")
189 .value(hour.to_string())
190 .expected("9 to 16")]);
191 }
192
193 Ok(())
194 })
195 }
196
197 pub fn weekends_only() -> ValidationRule {
198 ValidationRule::new("weekends_only")
199 .description("Only allows weekend dates (Saturday and Sunday)")
200 .with_date_validator(|date| {
201 if !date.is_weekend() {
202 Err(vec![ValidationError::new(
203 "weekends_only",
204 "Date must be a weekend (Saturday or Sunday)"
205 ).field("day_of_week")
206 .value(date.day_of_week().long_name().to_string())
207 .expected("Saturday or Sunday")])
208 } else {
209 Ok(())
210 }
211 })
212 }
213
214 pub fn no_holidays() -> ValidationRule {
215 ValidationRule::new("no_holidays")
216 .description("Excludes common holidays")
217 .with_date_validator(|date| {
218 // Simple holiday check - this should be expanded based on requirements
219 let is_common_holiday = (date.month() == 12 && date.day() == 25) || // Christmas
220 (date.month() == 1 && date.day() == 1) || // New Year
221 (date.month() == 7 && date.day() == 4); // Independence Day (US)
222
223 if is_common_holiday {
224 Err(vec![ValidationError::new(
225 "no_holidays",
226 "Date cannot be a holiday"
227 ).field("date")
228 .value(date.to_string())
229 .expected("Non-holiday date")])
230 } else {
231 Ok(())
232 }
233 })
234 }
235
236 pub fn allowed_weekdays(allowed_days: HashSet<crate::constant::DayOfWeek>) -> ValidationRule {
237 ValidationRule::new("allowed_weekdays")
238 .description("Only allows specific days of the week")
239 .with_date_validator(move |date| {
240 if !allowed_days.contains(&date.day_of_week()) {
241 let allowed_names: Vec<String> = allowed_days
242 .iter()
243 .map(|day| day.long_name().to_string())
244 .collect();
245
246 Err(vec![ValidationError::new(
247 "allowed_weekdays",
248 "Date must be one of the allowed weekdays"
249 ).field("day_of_week")
250 .value(date.day_of_week().long_name().to_string())
251 .expected(allowed_names.join(", "))])
252 } else {
253 Ok(())
254 }
255 })
256 }
257
258 /// Both hours are inclusive.
259 pub fn hour_range(min_hour: u8, max_hour: u8) -> ValidationRule {
260 ValidationRule::new("hour_range")
261 .description(format!("Only allows times between {} and {} hours", min_hour, max_hour))
262 .with_time_validator(move |time| {
263 let hour = time.hour().of();
264 if hour < min_hour || hour > max_hour {
265 Err(vec![ValidationError::new(
266 "hour_range",
267 "Hour must be within allowed range"
268 ).field("hour")
269 .value(hour.to_string())
270 .expected(format!("{} to {}", min_hour, max_hour))])
271 } else {
272 Ok(())
273 }
274 })
275 }
276
277 pub fn min_date(min_date: CalendarDate) -> ValidationRule {
278 ValidationRule::new("min_date")
279 .description(format!("Only allows dates on or after {}", min_date))
280 .with_date_validator(move |date| {
281 if date < &min_date {
282 Err(vec![ValidationError::new(
283 "min_date",
284 "Date must be on or after minimum date"
285 ).field("date")
286 .value(date.to_string())
287 .expected(format!("{} or later", min_date))])
288 } else {
289 Ok(())
290 }
291 })
292 }
293
294 pub fn max_date(max_date: CalendarDate) -> ValidationRule {
295 ValidationRule::new("max_date")
296 .description(format!("Only allows dates on or before {}", max_date))
297 .with_date_validator(move |date| {
298 if date > &max_date {
299 Err(vec![ValidationError::new(
300 "max_date",
301 "Date must be on or before maximum date"
302 ).field("date")
303 .value(date.to_string())
304 .expected(format!("{} or earlier", max_date))])
305 } else {
306 Ok(())
307 }
308 })
309 }
310
311 /// Both years are inclusive.
312 pub fn year_range(min_year: i32, max_year: i32) -> ValidationRule {
313 ValidationRule::new("year_range")
314 .description(format!("Only allows years between {} and {}", min_year, max_year))
315 .with_date_validator(move |date| {
316 let year = date.year();
317 if year < min_year || year > max_year {
318 Err(vec![ValidationError::new(
319 "year_range",
320 "Year must be within allowed range"
321 ).field("year")
322 .value(year.to_string())
323 .expected(format!("{} to {}", min_year, max_year))])
324 } else {
325 Ok(())
326 }
327 })
328 }
329
330 pub fn allowed_months(allowed_months: HashSet<crate::constant::MonthOfYear>) -> ValidationRule {
331 ValidationRule::new("allowed_months")
332 .description("Only allows specific months")
333 .with_date_validator(move |date| {
334 if !allowed_months.contains(&date.month_of_year()) {
335 let allowed_names: Vec<String> = allowed_months
336 .iter()
337 .map(|month| month.long_name().to_string())
338 .collect();
339
340 Err(vec![ValidationError::new(
341 "allowed_months",
342 "Month must be one of the allowed months"
343 ).field("month")
344 .value(date.month_of_year().long_name().to_string())
345 .expected(allowed_names.join(", "))])
346 } else {
347 Ok(())
348 }
349 })
350 }
351
352 pub fn whole_seconds_only() -> ValidationRule {
353 ValidationRule::new("whole_seconds_only")
354 .description("Only allows times with zero nanoseconds (whole seconds)")
355 .with_time_validator(|time| {
356 if time.nanosecond().of() != 0 {
357 Err(vec![ValidationError::new(
358 "whole_seconds_only",
359 "Time must have zero nanoseconds (whole seconds only)"
360 ).field("nanosecond")
361 .value(time.nanosecond().of().to_string())
362 .expected("0")])
363 } else {
364 Ok(())
365 }
366 })
367 }
368
369 pub fn whole_minutes_only() -> ValidationRule {
370 ValidationRule::new("whole_minutes_only")
371 .description("Only allows times with zero seconds and nanoseconds (whole minutes)")
372 .with_time_validator(|time| {
373 if time.second().of() != 0 || time.nanosecond().of() != 0 {
374 Err(vec![ValidationError::new(
375 "whole_minutes_only",
376 "Time must have zero seconds and nanoseconds (whole minutes only)"
377 ).field("time_precision")
378 .value(format!("{}s {}ns", time.second().of(), time.nanosecond().of()))
379 .expected("0s 0ns")])
380 } else {
381 Ok(())
382 }
383 })
384 }
385
386 fn get_current_year() -> i32 {
387 let now = SystemTime::now();
388 match now.duration_since(UNIX_EPOCH) {
389 Ok(duration) => {
390 // Convert seconds since Unix epoch to current year
391 // Approximate: 365.25 days per year, 86400 seconds per day
392 let years_since_1970 = duration.as_secs() / (365 * 24 * 60 * 60);
393 1970 + years_since_1970 as i32
394 },
395 Err(_) => {
396 // Fallback to a reasonable default if system time is before Unix epoch
397 2024
398 }
399 }
400 }
401
402 pub fn not_too_old(max_years_ago: u32) -> ValidationRule {
403 ValidationRule::new("not_too_old")
404 .description(format!("Prevents dates more than {} years in the past", max_years_ago))
405 .with_date_validator(move |date| {
406 let current_year = Self::get_current_year();
407 let min_allowed_year = current_year - max_years_ago as i32;
408
409 if date.year() < min_allowed_year {
410 Err(vec![ValidationError::new(
411 "not_too_old",
412 "Date cannot be too far in the past"
413 ).field("year")
414 .value(date.year().to_string())
415 .expected(format!("{} or later", min_allowed_year))])
416 } else {
417 Ok(())
418 }
419 })
420 }
421
422 pub fn not_too_future(max_years_ahead: u32) -> ValidationRule {
423 ValidationRule::new("not_too_future")
424 .description(format!("Prevents dates more than {} years in the future", max_years_ahead))
425 .with_date_validator(move |date| {
426 let current_year = Self::get_current_year();
427 let max_allowed_year = current_year + max_years_ahead as i32;
428
429 if date.year() > max_allowed_year {
430 Err(vec![ValidationError::new(
431 "not_too_future",
432 "Date cannot be too far in the future"
433 ).field("year")
434 .value(date.year().to_string())
435 .expected(format!("{} or earlier", max_allowed_year))])
436 } else {
437 Ok(())
438 }
439 })
440 }
441}
442
443// ========================================================================
444// Rule Combinators
445// ========================================================================
446
447/// Every rule must pass, and the errors of all that fail are returned.
448pub fn all_rules(rules: Vec<ValidationRule>) -> ValidationRule {
449 ValidationRule::new("combined_all")
450 .description("All specified rules must pass")
451 .with_calclock_validator(move |calclock| {
452 let mut all_errors = Vec::new();
453
454 for rule in &rules {
455 if let Err(mut errors) = rule.validate_calclock(calclock) {
456 all_errors.append(&mut errors);
457 }
458 }
459
460 if all_errors.is_empty() {
461 Ok(())
462 } else {
463 Err(all_errors)
464 }
465 })
466}
467
468/// One rule passing is enough.
469pub fn any_rule(rules: Vec<ValidationRule>) -> ValidationRule {
470 ValidationRule::new("combined_any")
471 .description("At least one of the specified rules must pass")
472 .with_calclock_validator(move |calclock| {
473 for rule in &rules {
474 if rule.validate_calclock(calclock).is_ok() {
475 return Ok(());
476 }
477 }
478
479 Err(vec![ValidationError::new(
480 "combined_any",
481 "None of the alternative validation rules passed"
482 )])
483 })
484}
485
486// ========================================================================
487// Common Rule Combinations
488// ========================================================================
489
490impl ValidationRules {
491 pub fn strict_business() -> ValidationRule {
492 let mut weekdays = HashSet::new();
493 weekdays.insert(crate::constant::DayOfWeek::Monday);
494 weekdays.insert(crate::constant::DayOfWeek::Tuesday);
495 weekdays.insert(crate::constant::DayOfWeek::Wednesday);
496 weekdays.insert(crate::constant::DayOfWeek::Thursday);
497 weekdays.insert(crate::constant::DayOfWeek::Friday);
498
499 all_rules(vec![
500 Self::business_hours(),
501 Self::no_holidays(),
502 Self::allowed_weekdays(weekdays),
503 ])
504 }
505
506 pub fn appointment_scheduling() -> ValidationRule {
507 all_rules(vec![
508 Self::business_hours(),
509 Self::whole_minutes_only(),
510 Self::not_too_old(1),
511 Self::not_too_future(2),
512 ])
513 }
514
515 pub fn historical_data() -> ValidationRule {
516 all_rules(vec![
517 Self::max_date({
518 // TODO: Replace with proper error handling for static initialization
519 let calendar = Calendar::new();
520 let utc_zone = crate::time::CalClockZone::utc();
521 match calendar.date(2024, 12, 31, utc_zone) {
522 Ok(date) => date,
523 Err(_) => panic!("Failed to create validation date"),
524 }
525 }),
526 Self::not_too_old(100),
527 ])
528 }
529}