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 {
158    use super::*;
159    use crate::base::validator::decimal::MaxDecimalPlaces;
160
161    struct TestCtx {
162        issues: crate::visitor::VisitorIssues,
163    }
164
165    impl TestCtx {
166        fn new() -> Self {
167            Self {
168                issues: crate::visitor::VisitorIssues::new(),
169            }
170        }
171    }
172
173    impl crate::visitor::VisitorContext for TestCtx {
174        fn add_issue(&mut self, issue: crate::visitor::Issue) {
175            self.issues.push(String::new(), issue);
176        }
177
178        fn add_issue_at(&mut self, _: crate::visitor::PathSegment, issue: crate::visitor::Issue) {
179            self.add_issue(issue);
180        }
181    }
182
183    fn assert_invalid_numeric_bound<N: NumericValue + Copy>(bound: N) {
184        let validators: &[&dyn Validator<u128>] = &[
185            &Lt::new(bound),
186            &Gt::new(bound),
187            &Lte::new(bound),
188            &Gte::new(bound),
189            &Equal::new(bound),
190            &NotEqual::new(bound),
191            &Range::new(bound, bound),
192            &MultipleOf::new(bound),
193        ];
194        for validator in validators {
195            for value in [0_u128, 1, 42] {
196                for _ in 0..2 {
197                    let mut ctx = TestCtx::new();
198                    validator.validate(&value, &mut ctx);
199                    assert_eq!(ctx.issues.get("").map(<[_]>::len), Some(1));
200                }
201            }
202        }
203    }
204
205    #[test]
206    fn invalid_numeric_validator_configuration_is_never_accepted() {
207        assert_invalid_numeric_bound(u128::MAX);
208        for bound in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, 1e40, -1e40] {
209            assert_invalid_numeric_bound(bound);
210        }
211        for bound in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX] {
212            assert_invalid_numeric_bound(bound);
213        }
214        for validator in [Range::new(0_u128, u128::MAX), Range::new(u128::MAX, 0)] {
215            let mut ctx = TestCtx::new();
216            validator.validate(&0_u128, &mut ctx);
217            assert_eq!(ctx.issues.get("").map(<[_]>::len), Some(1));
218        }
219    }
220
221    #[test]
222    fn invalid_numeric_decimal_places_configuration_is_never_accepted() {
223        for validator in [MaxDecimalPlaces::new(-1), MaxDecimalPlaces::new(u64::MAX)] {
224            for value in [Decimal::ZERO, Decimal::from_i128_with_scale(1235, 3)] {
225                let mut ctx = TestCtx::new();
226                validator.validate(&value, &mut ctx);
227                assert_eq!(ctx.issues.get("").map(<[_]>::len), Some(1));
228            }
229        }
230    }
231
232    #[test]
233    fn numeric_validator_boundaries_and_defaults_remain_valid() {
234        let maximum = Decimal::from_i128_with_scale(i128::MAX, 0);
235        for validator in [
236            &Gte::new(i128::MIN) as &dyn Validator<Decimal>,
237            &Lte::new(i128::MAX),
238            &Equal::new(i128::MAX as u128),
239            &Range::new(i128::MIN, i128::MAX),
240            &MultipleOf::new(i128::MAX),
241        ] {
242            let mut ctx = TestCtx::new();
243            validator.validate(&maximum, &mut ctx);
244            assert!(ctx.issues.is_empty());
245        }
246        let defaults: &[(&dyn Validator<i32>, &dyn Validator<i32>)] = &[
247            (&Lt::default(), &Lt::new(0)),
248            (&Gt::default(), &Gt::new(0)),
249            (&Lte::default(), &Lte::new(0)),
250            (&Gte::default(), &Gte::new(0)),
251            (&Equal::default(), &Equal::new(0)),
252            (&NotEqual::default(), &NotEqual::new(0)),
253            (&Range::default(), &Range::new(0, 0)),
254            (&MultipleOf::default(), &MultipleOf::new(0)),
255        ];
256        for (default, explicit) in defaults {
257            for value in [-1, 0, 1] {
258                let mut default_ctx = TestCtx::new();
259                let mut explicit_ctx = TestCtx::new();
260                default.validate(&value, &mut default_ctx);
261                explicit.validate(&value, &mut explicit_ctx);
262                assert_eq!(
263                    default_ctx.issues.is_empty(),
264                    explicit_ctx.issues.is_empty()
265                );
266            }
267        }
268        for (validator, value, valid) in [
269            (MaxDecimalPlaces::default(), Decimal::ZERO, true),
270            (
271                MaxDecimalPlaces::new(0),
272                Decimal::from_i128_with_scale(1, 1),
273                false,
274            ),
275            (MaxDecimalPlaces::new(u32::MAX), maximum, true),
276        ] {
277            let mut ctx = TestCtx::new();
278            validator.validate(&value, &mut ctx);
279            assert_eq!(ctx.issues.is_empty(), valid);
280        }
281    }
282
283    #[test]
284    fn lt() {
285        let v = Lt::new(10);
286        let mut ctx = TestCtx::new();
287
288        v.validate(&5, &mut ctx);
289        assert!(ctx.issues.is_empty());
290
291        v.validate(&10, &mut ctx);
292        assert!(!ctx.issues.is_empty());
293    }
294
295    #[test]
296    fn gte() {
297        let v = Gte::new(5);
298        let mut ctx = TestCtx::new();
299
300        v.validate(&5, &mut ctx);
301        assert!(ctx.issues.is_empty());
302
303        v.validate(&4, &mut ctx);
304        assert!(!ctx.issues.is_empty());
305    }
306
307    #[test]
308    fn range() {
309        let r = Range::new(1, 3);
310        let mut ctx = TestCtx::new();
311
312        r.validate(&2, &mut ctx);
313        assert!(ctx.issues.is_empty());
314
315        r.validate(&0, &mut ctx);
316        assert!(!ctx.issues.is_empty());
317    }
318
319    #[test]
320    fn multiple_of() {
321        let m = MultipleOf::new(5);
322        let mut ctx = TestCtx::new();
323
324        m.validate(&10, &mut ctx);
325        assert!(ctx.issues.is_empty());
326
327        m.validate(&11, &mut ctx);
328        assert!(!ctx.issues.is_empty());
329    }
330
331    #[test]
332    fn multiple_of_decimal_scale_alignment_preserves_validation() {
333        for scale in 1..=28 {
334            for sign in [-1_i128, 1] {
335                let value =
336                    Decimal::from_i128_with_scale(sign * (5 * 10_i128.pow(scale) + 1), scale);
337                for divisor in [-10_000_000_000_000_i64, 10_000_000_000_000] {
338                    let mut ctx = TestCtx::new();
339                    MultipleOf::new(divisor).validate(&value, &mut ctx);
340                    assert!(
341                        !ctx.issues.is_empty(),
342                        "{value} is not a multiple of {divisor}"
343                    );
344
345                    let mut ctx = TestCtx::new();
346                    MultipleOf::new(Decimal::from_i128_with_scale(sign, 28))
347                        .validate(&value, &mut ctx);
348                    assert!(
349                        ctx.issues.is_empty(),
350                        "every admitted decimal is a multiple of 1e-28"
351                    );
352                }
353            }
354        }
355        let mut ctx = TestCtx::new();
356        MultipleOf::new(-1).validate(&Decimal::from_i128_with_scale(i128::MIN, 0), &mut ctx);
357        assert!(ctx.issues.is_empty());
358        let mut ctx = TestCtx::new();
359        MultipleOf::new(0).validate(&Decimal::ZERO, &mut ctx);
360        assert!(!ctx.issues.is_empty());
361    }
362}