icydb/base/validator/
num.rs

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