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 *configuration* time.
11fn cast_decimal_cfg<N: NumericValue>(value: &N) -> Decimal {
12    try_cast_decimal(value).unwrap_or_default()
13}
14
15/// Convert a numeric value into Decimal during *validation* time.
16fn cast_decimal_val<N: NumericValue>(value: &N, ctx: &mut dyn VisitorContext) -> Option<Decimal> {
17    try_cast_decimal(value).or_else(|| {
18        ctx.issue(format!(
19            "value of type {} cannot be represented as Decimal",
20            type_name::<N>()
21        ));
22        None
23    })
24}
25
26// ============================================================================
27// Comparison validators
28// ============================================================================
29
30macro_rules! cmp_validator {
31    ($name:ident, $op:tt, $msg:expr) => {
32        #[validator]
33        pub struct $name {
34            target: Decimal,
35        }
36
37        impl $name {
38            pub fn new<N: NumericValue>(target: N) -> Self {
39                let target = cast_decimal_cfg(&target);
40
41                Self { target }
42            }
43        }
44
45        impl<N: NumericValue> Validator<N> for $name {
46            fn validate(&self, value: &N, ctx: &mut dyn VisitorContext) {
47                let Some(v) = cast_decimal_val(value, ctx) else { return };
48
49                if !(v $op self.target) {
50                    ctx.issue(format!($msg, v, self.target));
51                }
52            }
53        }
54    };
55}
56
57cmp_validator!(Lt, <, "{} must be < {}");
58cmp_validator!(Gt, >, "{} must be > {}");
59cmp_validator!(Lte, <=, "{} must be <= {}");
60cmp_validator!(Gte, >=, "{} must be >= {}");
61cmp_validator!(Equal, ==, "{} must be == {}");
62cmp_validator!(NotEqual, !=, "{} must be != {}");
63
64///
65/// Range
66///
67
68#[validator]
69pub struct Range {
70    min: Decimal,
71    max: Decimal,
72}
73
74impl Range {
75    pub fn new<N: NumericValue>(min: N, max: N) -> Self {
76        let min = cast_decimal_cfg(&min);
77        let max = cast_decimal_cfg(&max);
78
79        Self { min, max }
80    }
81}
82
83impl<N: NumericValue> Validator<N> for Range {
84    fn validate(&self, value: &N, ctx: &mut dyn VisitorContext) {
85        let Some(v) = cast_decimal_val(value, ctx) else {
86            return;
87        };
88
89        if v < self.min || v > self.max {
90            ctx.issue(format!("{v} must be between {} and {}", self.min, self.max));
91        }
92    }
93}
94
95///
96/// MultipleOf
97///
98
99#[validator]
100pub struct MultipleOf {
101    target: Decimal,
102}
103
104impl MultipleOf {
105    pub fn new<N: NumericValue>(target: N) -> Self {
106        let target = cast_decimal_cfg(&target);
107
108        Self { target }
109    }
110}
111
112impl<N: NumericValue> Validator<N> for MultipleOf {
113    fn validate(&self, value: &N, ctx: &mut dyn VisitorContext) {
114        if self.target.is_zero() {
115            ctx.issue("multipleOf target must be non-zero".to_string());
116            return;
117        }
118
119        let Some(v) = cast_decimal_val(value, ctx) else {
120            return;
121        };
122
123        if !(v % self.target).is_zero() {
124            ctx.issue(format!("{v} is not a multiple of {}", self.target));
125        }
126    }
127}
128
129///
130/// TESTS
131///
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    struct TestCtx {
138        issues: crate::visitor::VisitorIssues,
139    }
140
141    impl TestCtx {
142        fn new() -> Self {
143            Self {
144                issues: crate::visitor::VisitorIssues::new(),
145            }
146        }
147    }
148
149    impl crate::visitor::VisitorContext for TestCtx {
150        fn add_issue(&mut self, issue: crate::visitor::Issue) {
151            self.issues.push(String::new(), issue);
152        }
153
154        fn add_issue_at(&mut self, _: crate::visitor::PathSegment, issue: crate::visitor::Issue) {
155            self.add_issue(issue);
156        }
157    }
158
159    #[test]
160    fn lt() {
161        let v = Lt::new(10);
162        let mut ctx = TestCtx::new();
163
164        v.validate(&5, &mut ctx);
165        assert!(ctx.issues.is_empty());
166
167        v.validate(&10, &mut ctx);
168        assert!(!ctx.issues.is_empty());
169    }
170
171    #[test]
172    fn gte() {
173        let v = Gte::new(5);
174        let mut ctx = TestCtx::new();
175
176        v.validate(&5, &mut ctx);
177        assert!(ctx.issues.is_empty());
178
179        v.validate(&4, &mut ctx);
180        assert!(!ctx.issues.is_empty());
181    }
182
183    #[test]
184    fn range() {
185        let r = Range::new(1, 3);
186        let mut ctx = TestCtx::new();
187
188        r.validate(&2, &mut ctx);
189        assert!(ctx.issues.is_empty());
190
191        r.validate(&0, &mut ctx);
192        assert!(!ctx.issues.is_empty());
193    }
194
195    #[test]
196    fn multiple_of() {
197        let m = MultipleOf::new(5);
198        let mut ctx = TestCtx::new();
199
200        m.validate(&10, &mut ctx);
201        assert!(ctx.issues.is_empty());
202
203        m.validate(&11, &mut ctx);
204        assert!(!ctx.issues.is_empty());
205    }
206}