Skip to main content

icydb_model/base/validator/
num.rs

1//! Module: base::validator::num
2//!
3//! Responsibility: base validator definitions.
4//! Does not own: normalization policy, persistence, or schema mutation semantics.
5//! Boundary: reports typed visitor issues for facade schema values.
6
7use crate::{base::helper::try_cast_decimal, prelude::*, schema::NumericValue, visitor::Validator};
8use std::any::type_name;
9
10/// Convert a numeric value into Decimal during *validation* time.
11fn cast_decimal_val<N: NumericValue>(value: &N, ctx: &mut dyn VisitorContext) -> Option<Decimal> {
12    try_cast_decimal(value).or_else(|| {
13        ctx.issue(format!(
14            "value of type {} cannot be represented as Decimal",
15            type_name::<N>()
16        ));
17        None
18    })
19}
20
21// ============================================================================
22// Comparison validators
23// ============================================================================
24
25macro_rules! cmp_validator {
26    ($name:ident, $op:tt, $msg:expr) => {
27        #[validator]
28        pub struct $name {
29            target: Decimal,
30            invalid_configuration: bool,
31        }
32
33        impl $name {
34            /// Configure a bound; unrepresentable bounds report an issue on validation.
35            pub fn new<N: NumericValue>(target: N) -> Self {
36                let target = try_cast_decimal(&target);
37
38                Self {
39                    target: target.unwrap_or_default(),
40                    invalid_configuration: target.is_none(),
41                }
42            }
43        }
44
45        impl<N: NumericValue> Validator<N> for $name {
46            fn validate(&self, value: &N, ctx: &mut dyn VisitorContext) {
47                if self.invalid_configuration {
48                    ctx.issue("numeric validator bound cannot be represented as Decimal".to_string());
49                    return;
50                }
51                let Some(v) = cast_decimal_val(value, ctx) else { return };
52
53                if !(v $op self.target) {
54                    ctx.issue(format!($msg, v, self.target));
55                }
56            }
57        }
58    };
59}
60
61cmp_validator!(Lt, <, "{} must be < {}");
62cmp_validator!(Gt, >, "{} must be > {}");
63cmp_validator!(Lte, <=, "{} must be <= {}");
64cmp_validator!(Gte, >=, "{} must be >= {}");
65cmp_validator!(Equal, ==, "{} must be == {}");
66cmp_validator!(NotEqual, !=, "{} must be != {}");
67
68///
69/// Range
70///
71
72#[validator]
73pub struct Range {
74    min: Decimal,
75    max: Decimal,
76    invalid_configuration: bool,
77}
78
79impl Range {
80    /// Configure bounds; unrepresentable bounds report an issue on validation.
81    pub fn new<N: NumericValue>(min: N, max: N) -> Self {
82        let min = try_cast_decimal(&min);
83        let max = try_cast_decimal(&max);
84
85        Self {
86            min: min.unwrap_or_default(),
87            max: max.unwrap_or_default(),
88            invalid_configuration: min.is_none() || max.is_none(),
89        }
90    }
91}
92
93impl<N: NumericValue> Validator<N> for Range {
94    fn validate(&self, value: &N, ctx: &mut dyn VisitorContext) {
95        if self.invalid_configuration {
96            ctx.issue("numeric validator bounds cannot be represented as Decimal".to_string());
97            return;
98        }
99        let Some(v) = cast_decimal_val(value, ctx) else {
100            return;
101        };
102
103        if v < self.min || v > self.max {
104            ctx.issue(format!("{v} must be between {} and {}", self.min, self.max));
105        }
106    }
107}
108
109///
110/// MultipleOf
111///
112
113#[validator]
114pub struct MultipleOf {
115    target: Decimal,
116    invalid_configuration: bool,
117}
118
119impl MultipleOf {
120    /// Configure a divisor; unrepresentable divisors report an issue on validation.
121    pub fn new<N: NumericValue>(target: N) -> Self {
122        let target = try_cast_decimal(&target);
123
124        Self {
125            target: target.unwrap_or_default(),
126            invalid_configuration: target.is_none(),
127        }
128    }
129}
130
131impl<N: NumericValue> Validator<N> for MultipleOf {
132    fn validate(&self, value: &N, ctx: &mut dyn VisitorContext) {
133        if self.invalid_configuration {
134            ctx.issue("multipleOf target cannot be represented as Decimal".to_string());
135            return;
136        }
137        if self.target.is_zero() {
138            ctx.issue("multipleOf target must be non-zero".to_string());
139            return;
140        }
141
142        let Some(v) = cast_decimal_val(value, ctx) else {
143            return;
144        };
145
146        if !(v % self.target).is_zero() {
147            ctx.issue(format!("{v} is not a multiple of {}", self.target));
148        }
149    }
150}
151
152///
153/// TESTS
154///
155
156#[cfg(test)]
157mod tests;