scirs2_core/validation/data/
constraints.rs

1//! Constraint types and validation logic
2//!
3//! This module provides various constraint types used for data validation,
4//! including range constraints, pattern matching, and statistical constraints.
5
6use std::time::Duration;
7
8use serde::{Deserialize, Serialize};
9
10/// Validation constraints for data fields
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub enum Constraint {
13    /// Value must be within range (inclusive)
14    Range { min: f64, max: f64 },
15    /// String must match pattern
16    Pattern(String),
17    /// Value must be one of the allowed values
18    AllowedValues(Vec<String>),
19    /// String length constraints
20    Length { min: usize, max: usize },
21    /// Numeric precision constraints
22    Precision { decimal_places: usize },
23    /// Uniqueness constraint
24    Unique,
25    /// Non-null constraint
26    NotNull,
27    /// Custom validation rule
28    Custom(String),
29    /// Array element constraints
30    ArrayElements(Box<Constraint>),
31    /// Array size constraints
32    ArraySize { min: usize, max: usize },
33    /// Statistical constraints for numeric data
34    Statistical(StatisticalConstraints),
35    /// Time-based constraints
36    Temporal(TimeConstraints),
37    /// Matrix/array shape constraints
38    Shape(ShapeConstraints),
39    /// Logical AND of multiple constraints - all must pass
40    And(Vec<Constraint>),
41    /// Logical OR of multiple constraints - at least one must pass
42    Or(Vec<Constraint>),
43    /// Logical NOT of a constraint - must not pass
44    Not(Box<Constraint>),
45    /// Conditional constraint - if condition passes, then constraint must pass
46    If {
47        condition: Box<Constraint>,
48        then_constraint: Box<Constraint>,
49        else_constraint: Option<Box<Constraint>>,
50    },
51}
52
53/// Statistical constraints for numeric data
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub struct StatisticalConstraints {
56    /// Minimum allowed mean value
57    pub min_mean: Option<f64>,
58    /// Maximum allowed mean value
59    pub max_mean: Option<f64>,
60    /// Minimum allowed standard deviation
61    pub min_std: Option<f64>,
62    /// Maximum allowed standard deviation
63    pub max_std: Option<f64>,
64    /// Expected statistical distribution
65    pub expected_distribution: Option<String>,
66}
67
68/// Shape constraints for arrays and matrices
69#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
70pub struct ShapeConstraints {
71    /// Exact dimensions required (None = any size for that dimension)
72    pub dimensions: Vec<Option<usize>>,
73    /// Minimum number of elements
74    pub min_elements: Option<usize>,
75    /// Maximum number of elements
76    pub max_elements: Option<usize>,
77    /// Whether matrix must be square (for 2D only)
78    pub require_square: bool,
79    /// Whether to allow broadcasting-compatible shapes
80    pub allow_broadcasting: bool,
81}
82
83/// Time series constraints
84#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
85pub struct TimeConstraints {
86    /// Minimum time interval between samples
87    pub min_interval: Option<Duration>,
88    /// Maximum time interval between samples
89    pub max_interval: Option<Duration>,
90    /// Whether timestamps must be monotonic
91    pub require_monotonic: bool,
92    /// Whether to allow duplicate timestamps
93    pub allow_duplicates: bool,
94}
95
96/// Sparse matrix formats
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
98pub enum SparseFormat {
99    /// Compressed Sparse Row
100    CSR,
101    /// Compressed Sparse Column
102    CSC,
103    /// Coordinate format (COO)
104    COO,
105    /// Dictionary of Keys
106    DOK,
107}
108
109/// Element validation function type
110pub type ElementValidatorFn<T> = Box<dyn Fn(&T) -> bool + Send + Sync>;
111
112/// Array validation constraints
113pub struct ArrayValidationConstraints {
114    /// Expected array shape
115    pub expectedshape: Option<Vec<usize>>,
116    /// Field name for error reporting
117    pub fieldname: Option<String>,
118    /// Check for NaN and infinity values
119    pub check_numeric_quality: bool,
120    /// Statistical constraints
121    pub statistical_constraints: Option<StatisticalConstraints>,
122    /// Check performance characteristics
123    pub check_performance: bool,
124    /// Element-wise validation function
125    pub element_validator: Option<ElementValidatorFn<f64>>,
126}
127
128impl ArrayValidationConstraints {
129    /// Create new array validation constraints
130    pub fn new() -> Self {
131        Self {
132            expectedshape: None,
133            fieldname: None,
134            check_numeric_quality: false,
135            statistical_constraints: None,
136            check_performance: false,
137            element_validator: None,
138        }
139    }
140
141    /// Set expected shape
142    pub fn withshape(mut self, shape: Vec<usize>) -> Self {
143        self.expectedshape = Some(shape);
144        self
145    }
146
147    /// Set field name
148    pub fn with_fieldname(mut self, name: &str) -> Self {
149        self.fieldname = Some(name.to_string());
150        self
151    }
152
153    /// Enable numeric quality checks
154    pub fn check_numeric_quality(mut self) -> Self {
155        self.check_numeric_quality = true;
156        self
157    }
158
159    /// Set statistical constraints
160    pub fn with_statistical_constraints(mut self, constraints: StatisticalConstraints) -> Self {
161        self.statistical_constraints = Some(constraints);
162        self
163    }
164
165    /// Enable performance checks
166    pub fn check_performance(mut self) -> Self {
167        self.check_performance = true;
168        self
169    }
170}
171
172impl Default for ArrayValidationConstraints {
173    fn default() -> Self {
174        Self::new()
175    }
176}
177
178impl StatisticalConstraints {
179    /// Create new statistical constraints
180    pub fn new() -> Self {
181        Self {
182            min_mean: None,
183            max_mean: None,
184            min_std: None,
185            max_std: None,
186            expected_distribution: None,
187        }
188    }
189
190    /// Set mean range
191    pub fn with_mean_range(mut self, min: f64, max: f64) -> Self {
192        self.min_mean = Some(min);
193        self.max_mean = Some(max);
194        self
195    }
196
197    /// Set standard deviation range
198    pub fn with_std_range(mut self, min: f64, max: f64) -> Self {
199        self.min_std = Some(min);
200        self.max_std = Some(max);
201        self
202    }
203
204    /// Set expected distribution
205    pub fn with_distribution(mut self, distribution: &str) -> Self {
206        self.expected_distribution = Some(distribution.to_string());
207        self
208    }
209}
210
211impl Default for StatisticalConstraints {
212    fn default() -> Self {
213        Self::new()
214    }
215}
216
217impl ShapeConstraints {
218    /// Create new shape constraints
219    pub fn new() -> Self {
220        Self {
221            dimensions: Vec::new(),
222            min_elements: None,
223            max_elements: None,
224            require_square: false,
225            allow_broadcasting: false,
226        }
227    }
228
229    /// Set exact dimensions
230    pub fn with_dimensions(mut self, dimensions: Vec<Option<usize>>) -> Self {
231        self.dimensions = dimensions;
232        self
233    }
234
235    /// Set element count range
236    pub fn with_element_range(mut self, min: usize, max: usize) -> Self {
237        self.min_elements = Some(min);
238        self.max_elements = Some(max);
239        self
240    }
241
242    /// Require square matrix
243    pub fn require_square(mut self) -> Self {
244        self.require_square = true;
245        self
246    }
247
248    /// Allow broadcasting
249    pub fn allow_broadcasting(mut self) -> Self {
250        self.allow_broadcasting = true;
251        self
252    }
253}
254
255impl Default for ShapeConstraints {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261impl TimeConstraints {
262    /// Create new time constraints
263    pub fn new() -> Self {
264        Self {
265            min_interval: None,
266            max_interval: None,
267            require_monotonic: false,
268            allow_duplicates: true,
269        }
270    }
271
272    /// Set interval range
273    pub fn with_interval_range(mut self, min: Duration, max: Duration) -> Self {
274        self.min_interval = Some(min);
275        self.max_interval = Some(max);
276        self
277    }
278
279    /// Set minimum time interval
280    pub fn with_min_interval(mut self, interval: Duration) -> Self {
281        self.min_interval = Some(interval);
282        self
283    }
284
285    /// Set maximum time interval
286    pub fn with_max_interval(mut self, interval: Duration) -> Self {
287        self.max_interval = Some(interval);
288        self
289    }
290
291    /// Require monotonic timestamps
292    pub fn require_monotonic(mut self) -> Self {
293        self.require_monotonic = true;
294        self
295    }
296
297    /// Disallow duplicate timestamps
298    pub fn disallow_duplicates(mut self) -> Self {
299        self.allow_duplicates = false;
300        self
301    }
302}
303
304impl Default for TimeConstraints {
305    fn default() -> Self {
306        Self::new()
307    }
308}
309
310/// Builder for composing constraints
311pub struct ConstraintBuilder {
312    constraints: Vec<Constraint>,
313}
314
315impl ConstraintBuilder {
316    /// Create a new constraint builder
317    pub fn new() -> Self {
318        Self {
319            constraints: Vec::new(),
320        }
321    }
322
323    /// Add a constraint to the builder
324    #[allow(clippy::should_implement_trait)]
325    pub fn add(mut self, constraint: Constraint) -> Self {
326        self.constraints.push(constraint);
327        self
328    }
329
330    /// Add a range constraint
331    pub fn range(self, min: f64, max: f64) -> Self {
332        self.add(Constraint::Range { min, max })
333    }
334
335    /// Add a pattern constraint
336    pub fn pattern(self, pattern: &str) -> Self {
337        self.add(Constraint::Pattern(pattern.to_string()))
338    }
339
340    /// Add a length constraint
341    pub fn length(self, min: usize, max: usize) -> Self {
342        self.add(Constraint::Length { min, max })
343    }
344
345    /// Add a not-null constraint
346    pub fn not_null(self) -> Self {
347        self.add(Constraint::NotNull)
348    }
349
350    /// Build an AND constraint from all added constraints
351    pub fn and(self) -> Constraint {
352        match self.constraints.len() {
353            0 => panic!("Cannot create AND constraint with no constraints"),
354            1 => self
355                .constraints
356                .into_iter()
357                .next()
358                .expect("Operation failed"),
359            _ => Constraint::And(self.constraints),
360        }
361    }
362
363    /// Build an OR constraint from all added constraints
364    pub fn or(self) -> Constraint {
365        match self.constraints.len() {
366            0 => panic!("Cannot create OR constraint with no constraints"),
367            1 => self
368                .constraints
369                .into_iter()
370                .next()
371                .expect("Operation failed"),
372            _ => Constraint::Or(self.constraints),
373        }
374    }
375}
376
377impl Default for ConstraintBuilder {
378    fn default() -> Self {
379        Self::new()
380    }
381}
382
383impl Constraint {
384    /// Create a constraint that requires all of the given constraints to pass
385    pub fn all_of(constraints: Vec<Constraint>) -> Self {
386        Constraint::And(constraints)
387    }
388
389    /// Create a constraint that requires at least one of the given constraints to pass
390    pub fn any_of(constraints: Vec<Constraint>) -> Self {
391        Constraint::Or(constraints)
392    }
393
394    /// Create a constraint that requires the given constraint to not pass
395    #[allow(clippy::should_implement_trait)]
396    pub fn not(constraint: Constraint) -> Self {
397        Constraint::Not(Box::new(constraint))
398    }
399
400    /// Create a conditional constraint
401    pub fn if_then(
402        condition: Constraint,
403        then_constraint: Constraint,
404        else_constraint: Option<Constraint>,
405    ) -> Self {
406        Constraint::If {
407            condition: Box::new(condition),
408            then_constraint: Box::new(then_constraint),
409            else_constraint: else_constraint.map(Box::new),
410        }
411    }
412
413    /// Chain this constraint with another using AND logic
414    pub fn and(self, other: Constraint) -> Self {
415        match self {
416            Constraint::And(mut constraints) => {
417                constraints.push(other);
418                Constraint::And(constraints)
419            }
420            _ => Constraint::And(vec![self, other]),
421        }
422    }
423
424    /// Chain this constraint with another using OR logic
425    pub fn or(self, other: Constraint) -> Self {
426        match self {
427            Constraint::Or(mut constraints) => {
428                constraints.push(other);
429                Constraint::Or(constraints)
430            }
431            _ => Constraint::Or(vec![self, other]),
432        }
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    #[test]
441    fn test_range_constraint() {
442        let constraint = Constraint::Range {
443            min: 0.0,
444            max: 100.0,
445        };
446        match constraint {
447            Constraint::Range { min, max } => {
448                assert_eq!(min, 0.0);
449                assert_eq!(max, 100.0);
450            }
451            _ => panic!("Expected Range constraint"),
452        }
453    }
454
455    #[test]
456    fn test_statistical_constraints() {
457        let constraints = StatisticalConstraints::new()
458            .with_mean_range(0.0, 10.0)
459            .with_std_range(1.0, 5.0)
460            .with_distribution("normal");
461
462        assert_eq!(constraints.min_mean, Some(0.0));
463        assert_eq!(constraints.max_mean, Some(10.0));
464        assert_eq!(constraints.min_std, Some(1.0));
465        assert_eq!(constraints.max_std, Some(5.0));
466        assert_eq!(
467            constraints.expected_distribution,
468            Some("normal".to_string())
469        );
470    }
471
472    #[test]
473    fn testshape_constraints() {
474        let constraints = ShapeConstraints::new()
475            .with_dimensions(vec![Some(10), Some(20)])
476            .with_element_range(100, 500)
477            .require_square();
478
479        assert_eq!(constraints.dimensions, vec![Some(10), Some(20)]);
480        assert_eq!(constraints.min_elements, Some(100));
481        assert_eq!(constraints.max_elements, Some(500));
482        assert!(constraints.require_square);
483    }
484
485    #[test]
486    fn test_constraint_builder() {
487        // Test AND composition
488        let constraint = ConstraintBuilder::new().range(0.0, 100.0).not_null().and();
489
490        match constraint {
491            Constraint::And(constraints) => {
492                assert_eq!(constraints.len(), 2);
493            }
494            _ => panic!("Expected And constraint"),
495        }
496
497        // Test OR composition
498        let constraint = ConstraintBuilder::new()
499            .pattern("^[a-z]+$")
500            .pattern("^[A-Z]+$")
501            .or();
502
503        match constraint {
504            Constraint::Or(constraints) => {
505                assert_eq!(constraints.len(), 2);
506            }
507            _ => panic!("Expected Or constraint"),
508        }
509    }
510
511    #[test]
512    fn test_constraint_chaining() {
513        // Test AND chaining
514        let constraint = Constraint::Range {
515            min: 0.0,
516            max: 100.0,
517        }
518        .and(Constraint::NotNull);
519
520        match constraint {
521            Constraint::And(constraints) => {
522                assert_eq!(constraints.len(), 2);
523            }
524            _ => panic!("Expected And constraint"),
525        }
526
527        // Test OR chaining
528        let constraint = Constraint::Pattern("^[a-z]+$".to_string())
529            .or(Constraint::Pattern("^[A-Z]+$".to_string()));
530
531        match constraint {
532            Constraint::Or(constraints) => {
533                assert_eq!(constraints.len(), 2);
534            }
535            _ => panic!("Expected Or constraint"),
536        }
537    }
538
539    #[test]
540    fn test_composite_constraints() {
541        // Test NOT constraint
542        let constraint = Constraint::not(Constraint::Pattern("forbidden".to_string()));
543        match constraint {
544            Constraint::Not(_) => {}
545            _ => panic!("Expected Not constraint"),
546        }
547
548        // Test IF-THEN constraint
549        let constraint = Constraint::if_then(
550            Constraint::NotNull,
551            Constraint::Range {
552                min: 0.0,
553                max: 100.0,
554            },
555            Some(Constraint::Pattern("N/A".to_string())),
556        );
557
558        match constraint {
559            Constraint::If {
560                condition: _,
561                then_constraint: _,
562                else_constraint,
563            } => {
564                assert!(else_constraint.is_some());
565            }
566            _ => panic!("Expected If constraint"),
567        }
568    }
569
570    #[test]
571    fn test_complex_composition() {
572        // Test complex nested constraints
573        let age_constraint = Constraint::all_of(vec![
574            Constraint::Range {
575                min: 0.0,
576                max: 150.0,
577            },
578            Constraint::NotNull,
579        ]);
580
581        let name_constraint = Constraint::any_of(vec![
582            Constraint::Pattern("^[A-Za-z ]+$".to_string()),
583            Constraint::Pattern("^[\\p{L} ]+$".to_string()),
584        ]);
585
586        // Combine constraints
587        let combined = Constraint::And(vec![age_constraint, name_constraint]);
588
589        match combined {
590            Constraint::And(constraints) => {
591                assert_eq!(constraints.len(), 2);
592            }
593            _ => panic!("Expected And constraint"),
594        }
595    }
596
597    #[test]
598    fn test_time_constraints() {
599        let constraints = TimeConstraints::new()
600            .with_min_interval(Duration::from_secs(1))
601            .with_max_interval(Duration::from_secs(60))
602            .require_monotonic()
603            .disallow_duplicates();
604
605        assert_eq!(constraints.min_interval, Some(Duration::from_secs(1)));
606        assert_eq!(constraints.max_interval, Some(Duration::from_secs(60)));
607        assert!(constraints.require_monotonic);
608        assert!(!constraints.allow_duplicates);
609    }
610
611    #[test]
612    fn test_constraint_builder_edge_cases() {
613        // Test single constraint with and()
614        let constraint = ConstraintBuilder::new().range(0.0, 100.0).and();
615
616        match constraint {
617            Constraint::Range { min, max } => {
618                assert_eq!(min, 0.0);
619                assert_eq!(max, 100.0);
620            }
621            _ => panic!("Expected Range constraint, not And"),
622        }
623
624        // Test empty builder should panic
625        let result = std::panic::catch_unwind(|| ConstraintBuilder::new().and());
626        assert!(result.is_err());
627
628        // Test builder with all constraint types
629        let constraint = ConstraintBuilder::new()
630            .range(0.0, 100.0)
631            .pattern("^[A-Z]+$")
632            .length(5, 10)
633            .not_null()
634            .and();
635
636        match constraint {
637            Constraint::And(constraints) => {
638                assert_eq!(constraints.len(), 4);
639            }
640            _ => panic!("Expected And constraint"),
641        }
642    }
643
644    #[test]
645    fn test_nested_constraint_composition() {
646        // Test deep nesting
647        let inner = Constraint::Range {
648            min: 0.0,
649            max: 50.0,
650        };
651        let middle = Constraint::And(vec![inner, Constraint::NotNull]);
652        let outer = Constraint::Or(vec![middle, Constraint::Pattern("special".to_string())]);
653        let complex = Constraint::Not(Box::new(outer));
654
655        match complex {
656            Constraint::Not(inner) => match inner.as_ref() {
657                Constraint::Or(constraints) => {
658                    assert_eq!(constraints.len(), 2);
659                }
660                _ => panic!("Expected Or constraint"),
661            },
662            _ => panic!("Expected Not constraint"),
663        }
664    }
665
666    #[test]
667    fn test_constraint_equality() {
668        let c1 = Constraint::Range {
669            min: 0.0,
670            max: 100.0,
671        };
672        let c2 = Constraint::Range {
673            min: 0.0,
674            max: 100.0,
675        };
676        let c3 = Constraint::Range {
677            min: 0.0,
678            max: 200.0,
679        };
680
681        assert_eq!(c1, c2);
682        assert_ne!(c1, c3);
683
684        let and1 = Constraint::And(vec![c1.clone(), Constraint::NotNull]);
685        let and2 = Constraint::And(vec![c2.clone(), Constraint::NotNull]);
686        assert_eq!(and1, and2);
687    }
688
689    #[test]
690    fn test_array_validation_constraints() {
691        let constraints = ArrayValidationConstraints::new()
692            .withshape(vec![10, 20])
693            .with_fieldname("test_array")
694            .check_numeric_quality()
695            .check_performance();
696
697        assert_eq!(constraints.expectedshape, Some(vec![10, 20]));
698        assert_eq!(constraints.fieldname, Some("test_array".to_string()));
699        assert!(constraints.check_numeric_quality);
700        assert!(constraints.check_performance);
701    }
702
703    #[test]
704    fn test_sparse_format() {
705        let format = SparseFormat::CSR;
706        assert_eq!(format, SparseFormat::CSR);
707    }
708}