Oregami
Repositories/oxedyne/fe2o3

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

15.7 KiB, 55 runs

created by r1870400018:8620, 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 that branches on a condition.
2//!
3//! A conditional rule holds a condition and a rule for each way it can go,
4//! which is how "extended hours at the weekend" or "holiday rules in
5//! December" are expressed.
6//!
7//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
8//! Anthropic Claude
9
10use crate::{
11 calendar::CalendarDate,
12 clock::ClockTime,
13 time::CalClock,
14 validation::{ValidationError, ValidationRule},
15 constant::{DayOfWeek, MonthOfYear},
16};
17
18use oxedyne_fe2o3_core::prelude::*;
19
20use std::collections::HashSet;
21
22/// # Examples
23///
24/// ```ignore
25/// use oxedyne_fe2o3_datime::validation::{ConditionalRule, ValidationCondition};
26///
27/// // Rule: Allow extended hours on weekends
28/// let weekend_rule = ConditionalRule::new("weekend_extended_hours")
29/// .condition(ValidationCondition::IsWeekend)
30/// .if_true(ValidationRules::hour_range(0, 23)) // 24 hour access
31/// .if_false(ValidationRules::hour_range(9, 17)); // Business hours only
32/// ```
33#[derive(Debug)]
34pub struct ConditionalRule {
35 name: String,
36 condition: ValidationCondition,
37 true_rule: Option<ValidationRule>,
38 false_rule: Option<ValidationRule>,
39 always_rules: Vec<ValidationRule>, // run whichever way it goes
40}
41
42impl ConditionalRule {
43 pub fn new<S: Into<String>>(name: S) -> Self {
44 Self {
45 name: name.into(),
46 condition: ValidationCondition::Always,
47 true_rule: None,
48 false_rule: None,
49 always_rules: Vec::new(),
50 }
51 }
52
53 pub fn condition(mut self, condition: ValidationCondition) -> Self {
54 self.condition = condition;
55 self
56 }
57
58 pub fn if_true(mut self, rule: ValidationRule) -> Self {
59 self.true_rule = Some(rule);
60 self
61 }
62
63 pub fn if_false(mut self, rule: ValidationRule) -> Self {
64 self.false_rule = Some(rule);
65 self
66 }
67
68 pub fn always(mut self, rule: ValidationRule) -> Self {
69 self.always_rules.push(rule);
70 self
71 }
72
73 /// The always rules run first, then whichever branch the condition
74 /// selects, and the errors of both are returned together.
75 pub fn into_rule(self) -> ValidationRule {
76 let name = self.name.clone();
77 let condition = self.condition;
78 let true_rule = self.true_rule;
79 let false_rule = self.false_rule;
80 let always_rules = self.always_rules;
81
82 ValidationRule::new(name)
83 .with_calclock_validator(move |calclock| {
84 let mut errors = Vec::new();
85
86 // Apply always rules first
87 for rule in &always_rules {
88 if let Err(mut rule_errors) = rule.validate_calclock(calclock) {
89 errors.append(&mut rule_errors);
90 }
91 }
92
93 // Evaluate condition and apply appropriate rule
94 let condition_met = condition.evaluate_calclock(calclock);
95
96 let applicable_rule = if condition_met {
97 &true_rule
98 } else {
99 &false_rule
100 };
101
102 if let Some(rule) = applicable_rule {
103 if let Err(mut rule_errors) = rule.validate_calclock(calclock) {
104 errors.append(&mut rule_errors);
105 }
106 }
107
108 if errors.is_empty() {
109 Ok(())
110 } else {
111 Err(errors)
112 }
113 })
114 }
115
116 pub fn name(&self) -> &str {
117 &self.name
118 }
119}
120
121pub enum ValidationCondition {
122 Always,
123 Never,
124 IsWeekend,
125 IsBusinessDay,
126 IsDayOfWeek(DayOfWeek),
127 IsMonth(MonthOfYear),
128 IsMonthIn(HashSet<MonthOfYear>),
129 // Ranges are inclusive at both ends.
130 IsHourInRange(u8, u8),
131 IsYearInRange(i32, i32),
132 IsDayInRange(u8, u8),
133 IsLeapYear,
134 IsAfterDate(i32, u8, u8), // year, month, day
135 IsBeforeDate(i32, u8, u8), // year, month, day
136 HasFractionalSeconds,
137 And(Vec<ValidationCondition>),
138 Or(Vec<ValidationCondition>),
139 Not(Box<ValidationCondition>),
140 Custom(Box<dyn Fn(&CalClock) -> bool + Send + Sync>),
141}
142
143impl Clone for ValidationCondition {
144 fn clone(&self) -> Self {
145 match self {
146 ValidationCondition::Always => ValidationCondition::Always,
147 ValidationCondition::Never => ValidationCondition::Never,
148 ValidationCondition::IsWeekend => ValidationCondition::IsWeekend,
149 ValidationCondition::IsBusinessDay => ValidationCondition::IsBusinessDay,
150 ValidationCondition::IsDayOfWeek(day) => ValidationCondition::IsDayOfWeek(*day),
151 ValidationCondition::IsMonth(month) => ValidationCondition::IsMonth(*month),
152 ValidationCondition::IsMonthIn(months) => ValidationCondition::IsMonthIn(months.clone()),
153 ValidationCondition::IsHourInRange(min, max) => ValidationCondition::IsHourInRange(*min, *max),
154 ValidationCondition::IsYearInRange(min, max) => ValidationCondition::IsYearInRange(*min, *max),
155 ValidationCondition::IsDayInRange(min, max) => ValidationCondition::IsDayInRange(*min, *max),
156 ValidationCondition::IsLeapYear => ValidationCondition::IsLeapYear,
157 ValidationCondition::IsAfterDate(y, m, d) => ValidationCondition::IsAfterDate(*y, *m, *d),
158 ValidationCondition::IsBeforeDate(y, m, d) => ValidationCondition::IsBeforeDate(*y, *m, *d),
159 ValidationCondition::HasFractionalSeconds => ValidationCondition::HasFractionalSeconds,
160 ValidationCondition::And(conditions) => ValidationCondition::And(conditions.clone()),
161 ValidationCondition::Or(conditions) => ValidationCondition::Or(conditions.clone()),
162 ValidationCondition::Not(condition) => ValidationCondition::Not(condition.clone()),
163 ValidationCondition::Custom(_) => {
164 // Custom conditions with closures cannot be cloned
165 panic!("Cannot clone ValidationCondition::Custom variant")
166 }
167 }
168 }
169}
170
171impl std::fmt::Debug for ValidationCondition {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 match self {
174 ValidationCondition::Always => write!(f, "Always"),
175 ValidationCondition::Never => write!(f, "Never"),
176 ValidationCondition::IsWeekend => write!(f, "IsWeekend"),
177 ValidationCondition::IsBusinessDay => write!(f, "IsBusinessDay"),
178 ValidationCondition::IsDayOfWeek(day) => write!(f, "IsDayOfWeek({:?})", day),
179 ValidationCondition::IsMonth(month) => write!(f, "IsMonth({:?})", month),
180 ValidationCondition::IsMonthIn(months) => write!(f, "IsMonthIn({:?})", months),
181 ValidationCondition::IsHourInRange(min, max) => write!(f, "IsHourInRange({}, {})", min, max),
182 ValidationCondition::IsYearInRange(min, max) => write!(f, "IsYearInRange({}, {})", min, max),
183 ValidationCondition::IsDayInRange(min, max) => write!(f, "IsDayInRange({}, {})", min, max),
184 ValidationCondition::IsLeapYear => write!(f, "IsLeapYear"),
185 ValidationCondition::IsAfterDate(y, m, d) => write!(f, "IsAfterDate({}, {}, {})", y, m, d),
186 ValidationCondition::IsBeforeDate(y, m, d) => write!(f, "IsBeforeDate({}, {}, {})", y, m, d),
187 ValidationCondition::HasFractionalSeconds => write!(f, "HasFractionalSeconds"),
188 ValidationCondition::And(conditions) => write!(f, "And({:?})", conditions),
189 ValidationCondition::Or(conditions) => write!(f, "Or({:?})", conditions),
190 ValidationCondition::Not(condition) => write!(f, "Not({:?})", condition),
191 ValidationCondition::Custom(_) => write!(f, "Custom(<function>)"),
192 }
193 }
194}
195
196impl ValidationCondition {
197 pub fn evaluate_calclock(&self, calclock: &CalClock) -> bool {
198 match self {
199 ValidationCondition::Always => true,
200 ValidationCondition::Never => false,
201 ValidationCondition::IsWeekend => calclock.date().is_weekend(),
202 ValidationCondition::IsBusinessDay => calclock.date().is_business_day(),
203 ValidationCondition::IsDayOfWeek(day) => calclock.date().day_of_week() == *day,
204 ValidationCondition::IsMonth(month) => calclock.date().month_of_year() == *month,
205 ValidationCondition::IsMonthIn(months) => months.contains(&calclock.date().month_of_year()),
206 ValidationCondition::IsHourInRange(min, max) => {
207 let hour = calclock.time().hour().of();
208 hour >= *min && hour <= *max
209 }
210 ValidationCondition::IsYearInRange(min, max) => {
211 let year = calclock.date().year();
212 year >= *min && year <= *max
213 }
214 ValidationCondition::IsDayInRange(min, max) => {
215 let day = calclock.date().day();
216 day >= *min && day <= *max
217 }
218 ValidationCondition::IsLeapYear => calclock.date().is_leap_year(),
219 ValidationCondition::IsAfterDate(year, month, day) => {
220 let date = calclock.date();
221 date.year() > *year ||
222 (date.year() == *year && date.month() > *month) ||
223 (date.year() == *year && date.month() == *month && date.day() > *day)
224 }
225 ValidationCondition::IsBeforeDate(year, month, day) => {
226 let date = calclock.date();
227 date.year() < *year ||
228 (date.year() == *year && date.month() < *month) ||
229 (date.year() == *year && date.month() == *month && date.day() < *day)
230 }
231 ValidationCondition::HasFractionalSeconds => {
232 calclock.time().nanosecond().of() > 0
233 }
234 ValidationCondition::And(conditions) => {
235 conditions.iter().all(|cond| cond.evaluate_calclock(calclock))
236 }
237 ValidationCondition::Or(conditions) => {
238 conditions.iter().any(|cond| cond.evaluate_calclock(calclock))
239 }
240 ValidationCondition::Not(condition) => {
241 !condition.evaluate_calclock(calclock)
242 }
243 ValidationCondition::Custom(func) => func(calclock),
244 }
245 }
246
247 /// The date is given midnight so that time conditions can be evaluated;
248 /// a condition on the clock will read as if it were midnight.
249 pub fn evaluate_date(&self, date: &CalendarDate) -> bool {
250 // For date-only evaluation, we create a minimal CalClock
251 // In a real implementation, you might want date-specific conditions
252 let zone = date.zone().clone();
253 if let Ok(time) = crate::clock::ClockTime::new(0, 0, 0, 0, zone.clone()) {
254 if let Ok(calclock) = crate::time::CalClock::from_date_time(date.clone(), time) {
255 return self.evaluate_calclock(&calclock);
256 }
257 }
258 false
259 }
260
261 /// The time is given 2024-01-01 for the same reason, so a condition on
262 /// the date says nothing useful here.
263 pub fn evaluate_time(&self, time: &ClockTime) -> bool {
264 // For time-only evaluation, we create a minimal CalClock with today's date
265 let zone = time.zone().clone();
266 if let Ok(date) = crate::calendar::CalendarDate::new(2024, 1, 1, zone.clone()) {
267 if let Ok(calclock) = crate::time::CalClock::from_date_time(date, time.clone()) {
268 return self.evaluate_calclock(&calclock);
269 }
270 }
271 false
272 }
273}
274
275pub struct ConditionalRuleBuilder {
276 rules: Vec<ConditionalRule>,
277}
278
279impl ConditionalRuleBuilder {
280 pub fn new() -> Self {
281 Self {
282 rules: Vec::new(),
283 }
284 }
285
286 pub fn rule(mut self, rule: ConditionalRule) -> Self {
287 self.rules.push(rule);
288 self
289 }
290
291 pub fn build(self, name: &str) -> ValidationRule {
292 // Convert all conditional rules to validation rules first
293 let rules: Vec<ValidationRule> = self.rules
294 .into_iter()
295 .map(|conditional_rule| conditional_rule.into_rule())
296 .collect();
297
298 ValidationRule::new(name)
299 .with_calclock_validator(move |calclock| {
300 let mut all_errors = Vec::new();
301
302 for rule in &rules {
303 if let Err(mut errors) = rule.validate_calclock(calclock) {
304 all_errors.append(&mut errors);
305 }
306 }
307
308 if all_errors.is_empty() {
309 Ok(())
310 } else {
311 Err(all_errors)
312 }
313 })
314 }
315}
316
317impl Default for ConditionalRuleBuilder {
318 fn default() -> Self {
319 Self::new()
320 }
321}
322
323// Helper functions for creating common conditional rules
324impl ConditionalRule {
325 pub fn business_hours_by_day() -> Self {
326 use crate::validation::ValidationRules;
327
328 ConditionalRule::new("business_hours_by_day")
329 .condition(ValidationCondition::IsBusinessDay)
330 .if_true(ValidationRules::hour_range(9, 17)) // 9 AM - 5 PM on weekdays
331 .if_false(ValidationRules::hour_range(10, 14)) // 10 AM - 2 PM on weekends
332 }
333
334 pub fn holiday_scheduling() -> Self {
335 use crate::validation::ValidationRules;
336
337 // December - require advance scheduling
338 let december_months = {
339 let mut months = HashSet::new();
340 months.insert(MonthOfYear::December);
341 months
342 };
343
344 ConditionalRule::new("holiday_scheduling")
345 .condition(ValidationCondition::IsMonthIn(december_months))
346 .if_true(ValidationRules::not_too_future(30)) // 30 days max in advance
347 .if_false(ValidationRules::not_too_future(365)) // 1 year max normally
348 }
349
350 pub fn seasonal_hours() -> Self {
351 use crate::validation::ValidationRules;
352
353 // Summer months (June, July, August) - extended hours
354 let summer_months = {
355 let mut months = HashSet::new();
356 months.insert(MonthOfYear::June);
357 months.insert(MonthOfYear::July);
358 months.insert(MonthOfYear::August);
359 months
360 };
361
362 ConditionalRule::new("seasonal_hours")
363 .condition(ValidationCondition::IsMonthIn(summer_months))
364 .if_true(ValidationRules::hour_range(8, 20)) // 8 AM - 8 PM in summer
365 .if_false(ValidationRules::hour_range(9, 18)) // 9 AM - 6 PM otherwise
366 }
367
368 pub fn leap_year_aware() -> Self {
369 ConditionalRule::new("leap_year_aware")
370 .condition(ValidationCondition::And(vec![
371 ValidationCondition::IsMonth(MonthOfYear::February),
372 ValidationCondition::IsDayInRange(29, 29),
373 ]))
374 .if_true(ValidationRule::new("require_leap_year").with_date_validator(|date| {
375 if date.is_leap_year() {
376 Ok(())
377 } else {
378 Err(vec![ValidationError::new(
379 "leap_year_required",
380 "February 29 is only valid in leap years"
381 )])
382 }
383 }))
384 }
385
386 pub fn maintenance_window() -> Self {
387 // Sunday 2 AM - 4 AM maintenance window
388 ConditionalRule::new("maintenance_window")
389 .condition(ValidationCondition::And(vec![
390 ValidationCondition::IsDayOfWeek(DayOfWeek::Sunday),
391 ValidationCondition::IsHourInRange(2, 4),
392 ]))
393 .if_true(ValidationRule::new("maintenance_blocked").with_calclock_validator(|_| {
394 Err(vec![ValidationError::new(
395 "maintenance_window",
396 "System maintenance window - operations not allowed"
397 )])
398 }))
399 }
400}