icydb_model/base/validator/
num.rs1use crate::{base::helper::try_cast_decimal, prelude::*, schema::NumericValue, visitor::Validator};
8use std::any::type_name;
9
10fn 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
21macro_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 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#[validator]
73pub struct Range {
74 min: Decimal,
75 max: Decimal,
76 invalid_configuration: bool,
77}
78
79impl Range {
80 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#[validator]
114pub struct MultipleOf {
115 target: Decimal,
116 invalid_configuration: bool,
117}
118
119impl MultipleOf {
120 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#[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}