Skip to main content

icydb/base/validator/
num.rs

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