Skip to main content

vld/primitives/
number.rs

1use serde_json::Value;
2
3use crate::error::{value_type_name, IssueCode, VldError};
4use crate::schema::VldSchema;
5
6#[derive(Clone)]
7enum NumberCheck {
8    Min(f64, String),
9    Max(f64, String),
10    Gt(f64, String),
11    Lt(f64, String),
12    Positive(String),
13    Negative(String),
14    NonNegative(String),
15    NonPositive(String),
16    Finite(String),
17    MultipleOf(f64, String),
18    Safe(String),
19}
20
21impl NumberCheck {
22    /// Stable key identifying the check category.
23    fn key(&self) -> &str {
24        match self {
25            NumberCheck::Min(..) => "too_small",
26            NumberCheck::Max(..) => "too_big",
27            NumberCheck::Gt(..) => "too_small",
28            NumberCheck::Lt(..) => "too_big",
29            NumberCheck::Positive(..) => "not_positive",
30            NumberCheck::Negative(..) => "not_negative",
31            NumberCheck::NonNegative(..) => "not_non_negative",
32            NumberCheck::NonPositive(..) => "not_non_positive",
33            NumberCheck::Finite(..) => "not_finite",
34            NumberCheck::MultipleOf(..) => "not_multiple_of",
35            NumberCheck::Safe(..) => "not_safe",
36        }
37    }
38
39    /// Replace the error message stored in this check.
40    fn set_message(&mut self, msg: String) {
41        match self {
42            NumberCheck::Min(_, ref mut m)
43            | NumberCheck::Max(_, ref mut m)
44            | NumberCheck::Gt(_, ref mut m)
45            | NumberCheck::Lt(_, ref mut m)
46            | NumberCheck::Positive(ref mut m)
47            | NumberCheck::Negative(ref mut m)
48            | NumberCheck::NonNegative(ref mut m)
49            | NumberCheck::NonPositive(ref mut m)
50            | NumberCheck::Finite(ref mut m)
51            | NumberCheck::MultipleOf(_, ref mut m)
52            | NumberCheck::Safe(ref mut m) => *m = msg,
53        }
54    }
55}
56
57/// Schema for number validation (`f64`). Created via [`vld::number()`](crate::number).
58///
59/// Use `.int()` to convert to integer validation (`i64`).
60///
61/// # Example
62/// ```
63/// use vld::prelude::*;
64///
65/// let schema = vld::number().min(0.0).max(100.0);
66/// let int_schema = vld::number().int().min(0).max(100);
67/// ```
68#[derive(Clone)]
69pub struct ZNumber {
70    checks: Vec<NumberCheck>,
71    coerce: bool,
72    custom_type_error: Option<String>,
73}
74
75impl ZNumber {
76    pub fn new() -> Self {
77        Self {
78            checks: vec![],
79            coerce: false,
80            custom_type_error: None,
81        }
82    }
83
84    /// Set a custom error message for type mismatch (when the input is not a number).
85    ///
86    /// # Example
87    /// ```
88    /// use vld::prelude::*;
89    /// let schema = vld::number().type_error("Must be a number!");
90    /// let err = schema.parse(r#""hello""#).unwrap_err();
91    /// assert!(err.issues[0].message.contains("Must be a number!"));
92    /// ```
93    pub fn type_error(mut self, msg: impl Into<String>) -> Self {
94        self.custom_type_error = Some(msg.into());
95        self
96    }
97
98    /// Override error messages in bulk by check key.
99    ///
100    /// The closure receives the check key (e.g. `"too_small"`, `"too_big"`,
101    /// `"not_positive"`, `"not_finite"`, `"not_multiple_of"`, `"not_safe"`)
102    /// and should return `Some(new_message)` to replace, or `None` to keep the original.
103    ///
104    /// # Example
105    /// ```
106    /// use vld::prelude::*;
107    /// let schema = vld::number().min(0.0).max(100.0)
108    ///     .with_messages(|key| match key {
109    ///         "too_small" => Some("Must be >= 0".into()),
110    ///         "too_big" => Some("Must be <= 100".into()),
111    ///         _ => None,
112    ///     });
113    /// ```
114    pub fn with_messages<F>(mut self, f: F) -> Self
115    where
116        F: Fn(&str) -> Option<String>,
117    {
118        for check in &mut self.checks {
119            if let Some(msg) = f(check.key()) {
120                check.set_message(msg);
121            }
122        }
123        self
124    }
125
126    /// Minimum value (inclusive). Alias: `gte`.
127    pub fn min(mut self, val: f64) -> Self {
128        self.checks.push(NumberCheck::Min(
129            val,
130            format!("Number must be at least {}", val),
131        ));
132        self
133    }
134
135    /// Maximum value (inclusive). Alias: `lte`.
136    pub fn max(mut self, val: f64) -> Self {
137        self.checks.push(NumberCheck::Max(
138            val,
139            format!("Number must be at most {}", val),
140        ));
141        self
142    }
143
144    /// Greater than (exclusive).
145    pub fn gt(mut self, val: f64) -> Self {
146        self.checks.push(NumberCheck::Gt(
147            val,
148            format!("Number must be greater than {}", val),
149        ));
150        self
151    }
152
153    /// Greater than or equal (inclusive). Same as `min`.
154    pub fn gte(self, val: f64) -> Self {
155        self.min(val)
156    }
157
158    /// Less than (exclusive).
159    pub fn lt(mut self, val: f64) -> Self {
160        self.checks.push(NumberCheck::Lt(
161            val,
162            format!("Number must be less than {}", val),
163        ));
164        self
165    }
166
167    /// Less than or equal (inclusive). Same as `max`.
168    pub fn lte(self, val: f64) -> Self {
169        self.max(val)
170    }
171
172    /// Must be positive (> 0).
173    pub fn positive(mut self) -> Self {
174        self.checks
175            .push(NumberCheck::Positive("Number must be positive".to_string()));
176        self
177    }
178
179    /// Must be negative (< 0).
180    pub fn negative(mut self) -> Self {
181        self.checks
182            .push(NumberCheck::Negative("Number must be negative".to_string()));
183        self
184    }
185
186    /// Must be non-negative (>= 0).
187    pub fn non_negative(mut self) -> Self {
188        self.checks.push(NumberCheck::NonNegative(
189            "Number must be non-negative".to_string(),
190        ));
191        self
192    }
193
194    /// Must be non-positive (<= 0).
195    pub fn non_positive(mut self) -> Self {
196        self.checks.push(NumberCheck::NonPositive(
197            "Number must be non-positive".to_string(),
198        ));
199        self
200    }
201
202    /// Must be finite (not NaN or infinity).
203    pub fn finite(mut self) -> Self {
204        self.checks
205            .push(NumberCheck::Finite("Number must be finite".to_string()));
206        self
207    }
208
209    /// Must be a multiple of the given value.
210    pub fn multiple_of(mut self, val: f64) -> Self {
211        self.checks.push(NumberCheck::MultipleOf(
212            val,
213            format!("Number must be a multiple of {}", val),
214        ));
215        self
216    }
217
218    /// Must be within JavaScript's safe integer range (`-(2^53 - 1)` to `2^53 - 1`).
219    pub fn safe(mut self) -> Self {
220        self.checks.push(NumberCheck::Safe(
221            "Number must be a safe integer (-(2^53-1) to 2^53-1)".to_string(),
222        ));
223        self
224    }
225
226    /// Convert to integer validation. Returns `ZInt` with `Output = i64`.
227    pub fn int(self) -> ZInt {
228        ZInt {
229            inner: self,
230            custom_int_error: None,
231        }
232    }
233
234    /// Coerce strings and booleans to numbers.
235    pub fn coerce(mut self) -> Self {
236        self.coerce = true;
237        self
238    }
239
240    fn extract_number(&self, value: &Value) -> Result<f64, VldError> {
241        let type_err = |value: &Value| -> VldError {
242            let msg = self
243                .custom_type_error
244                .clone()
245                .unwrap_or_else(|| format!("Expected number, received {}", value_type_name(value)));
246            VldError::single_with_value(
247                IssueCode::InvalidType {
248                    expected: "number".to_string(),
249                    received: value_type_name(value),
250                },
251                msg,
252                value,
253            )
254        };
255
256        if let Some(n) = value.as_f64() {
257            Ok(n)
258        } else if self.coerce {
259            match value {
260                Value::String(s) => s.parse::<f64>().map_err(|_| {
261                    let msg = self
262                        .custom_type_error
263                        .clone()
264                        .unwrap_or_else(|| format!("Cannot coerce \"{}\" to number", s));
265                    VldError::single_with_value(
266                        IssueCode::InvalidType {
267                            expected: "number".to_string(),
268                            received: "string".to_string(),
269                        },
270                        msg,
271                        value,
272                    )
273                }),
274                Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
275                _ => Err(type_err(value)),
276            }
277        } else {
278            Err(type_err(value))
279        }
280    }
281
282    fn validate_number(&self, n: f64, value: &Value) -> Result<f64, VldError> {
283        let mut errors = VldError::new();
284
285        for check in &self.checks {
286            match check {
287                NumberCheck::Min(min, msg) => {
288                    if n < *min {
289                        errors.push_with_value(
290                            IssueCode::TooSmall {
291                                minimum: *min,
292                                inclusive: true,
293                            },
294                            msg.clone(),
295                            value,
296                        );
297                    }
298                }
299                NumberCheck::Max(max, msg) => {
300                    if n > *max {
301                        errors.push_with_value(
302                            IssueCode::TooBig {
303                                maximum: *max,
304                                inclusive: true,
305                            },
306                            msg.clone(),
307                            value,
308                        );
309                    }
310                }
311                NumberCheck::Gt(val, msg) => {
312                    if n <= *val {
313                        errors.push_with_value(
314                            IssueCode::TooSmall {
315                                minimum: *val,
316                                inclusive: false,
317                            },
318                            msg.clone(),
319                            value,
320                        );
321                    }
322                }
323                NumberCheck::Lt(val, msg) => {
324                    if n >= *val {
325                        errors.push_with_value(
326                            IssueCode::TooBig {
327                                maximum: *val,
328                                inclusive: false,
329                            },
330                            msg.clone(),
331                            value,
332                        );
333                    }
334                }
335                NumberCheck::Positive(msg) => {
336                    if n <= 0.0 {
337                        errors.push_with_value(
338                            IssueCode::TooSmall {
339                                minimum: 0.0,
340                                inclusive: false,
341                            },
342                            msg.clone(),
343                            value,
344                        );
345                    }
346                }
347                NumberCheck::Negative(msg) => {
348                    if n >= 0.0 {
349                        errors.push_with_value(
350                            IssueCode::TooBig {
351                                maximum: 0.0,
352                                inclusive: false,
353                            },
354                            msg.clone(),
355                            value,
356                        );
357                    }
358                }
359                NumberCheck::NonNegative(msg) => {
360                    if n < 0.0 {
361                        errors.push_with_value(
362                            IssueCode::TooSmall {
363                                minimum: 0.0,
364                                inclusive: true,
365                            },
366                            msg.clone(),
367                            value,
368                        );
369                    }
370                }
371                NumberCheck::NonPositive(msg) => {
372                    if n > 0.0 {
373                        errors.push_with_value(
374                            IssueCode::TooBig {
375                                maximum: 0.0,
376                                inclusive: true,
377                            },
378                            msg.clone(),
379                            value,
380                        );
381                    }
382                }
383                NumberCheck::Finite(msg) => {
384                    if !n.is_finite() {
385                        errors.push_with_value(IssueCode::NotFinite, msg.clone(), value);
386                    }
387                }
388                NumberCheck::MultipleOf(val, msg) => {
389                    if (n % val).abs() > f64::EPSILON {
390                        errors.push_with_value(
391                            IssueCode::Custom {
392                                code: "not_multiple_of".to_string(),
393                            },
394                            msg.clone(),
395                            value,
396                        );
397                    }
398                }
399                NumberCheck::Safe(msg) => {
400                    const MAX_SAFE: f64 = 9007199254740991.0;
401                    if !(-MAX_SAFE..=MAX_SAFE).contains(&n) {
402                        errors.push_with_value(
403                            IssueCode::Custom {
404                                code: "not_safe".to_string(),
405                            },
406                            msg.clone(),
407                            value,
408                        );
409                    }
410                }
411            }
412        }
413
414        if errors.is_empty() {
415            Ok(n)
416        } else {
417            Err(errors)
418        }
419    }
420}
421
422impl Default for ZNumber {
423    fn default() -> Self {
424        Self::new()
425    }
426}
427
428impl ZNumber {
429    /// Generate a JSON Schema representation of this number schema.
430    ///
431    /// Requires the `openapi` feature.
432    #[cfg(feature = "openapi")]
433    pub fn to_json_schema(&self) -> serde_json::Value {
434        let mut schema = serde_json::json!({"type": "number"});
435        for check in &self.checks {
436            match check {
437                NumberCheck::Min(n, _) => {
438                    schema["minimum"] = serde_json::json!(*n);
439                }
440                NumberCheck::Max(n, _) => {
441                    schema["maximum"] = serde_json::json!(*n);
442                }
443                NumberCheck::Gt(n, _) => {
444                    schema["exclusiveMinimum"] = serde_json::json!(*n);
445                }
446                NumberCheck::Lt(n, _) => {
447                    schema["exclusiveMaximum"] = serde_json::json!(*n);
448                }
449                NumberCheck::MultipleOf(n, _) => {
450                    schema["multipleOf"] = serde_json::json!(*n);
451                }
452                _ => {}
453            }
454        }
455        schema
456    }
457}
458
459impl VldSchema for ZNumber {
460    type Output = f64;
461
462    fn parse_value(&self, value: &Value) -> Result<f64, VldError> {
463        let n = self.extract_number(value)?;
464        self.validate_number(n, value)
465    }
466}
467
468// ---------------------------------------------------------------------------
469// ZInt — integer validation (i64)
470// ---------------------------------------------------------------------------
471
472/// Schema for integer validation (`i64`). Created via `vld::number().int()`.
473#[derive(Clone)]
474pub struct ZInt {
475    inner: ZNumber,
476    custom_int_error: Option<String>,
477}
478
479impl ZInt {
480    /// Set a custom error message for type mismatch (non-number input).
481    pub fn type_error(mut self, msg: impl Into<String>) -> Self {
482        self.inner = self.inner.type_error(msg);
483        self
484    }
485
486    /// Set a custom error message for when the number is not an integer.
487    ///
488    /// # Example
489    /// ```
490    /// use vld::prelude::*;
491    /// let schema = vld::number().int().int_error("Whole numbers only!");
492    /// let err = schema.parse("3.5").unwrap_err();
493    /// assert!(err.issues[0].message.contains("Whole numbers only!"));
494    /// ```
495    pub fn int_error(mut self, msg: impl Into<String>) -> Self {
496        self.custom_int_error = Some(msg.into());
497        self
498    }
499
500    /// Override error messages in bulk by check key.
501    ///
502    /// Same keys as [`ZNumber::with_messages`], plus `"not_int"` for the
503    /// integer check itself.
504    pub fn with_messages<F>(mut self, f: F) -> Self
505    where
506        F: Fn(&str) -> Option<String>,
507    {
508        if let Some(msg) = f("not_int") {
509            self.custom_int_error = Some(msg);
510        }
511        self.inner = self.inner.with_messages(f);
512        self
513    }
514
515    /// Minimum value (inclusive).
516    pub fn min(mut self, val: i64) -> Self {
517        self.inner.checks.push(NumberCheck::Min(
518            val as f64,
519            format!("Number must be at least {}", val),
520        ));
521        self
522    }
523
524    /// Maximum value (inclusive).
525    pub fn max(mut self, val: i64) -> Self {
526        self.inner.checks.push(NumberCheck::Max(
527            val as f64,
528            format!("Number must be at most {}", val),
529        ));
530        self
531    }
532
533    /// Greater than (exclusive).
534    pub fn gt(mut self, val: i64) -> Self {
535        self.inner.checks.push(NumberCheck::Gt(
536            val as f64,
537            format!("Number must be greater than {}", val),
538        ));
539        self
540    }
541
542    /// Greater than or equal (inclusive). Same as `min`.
543    pub fn gte(self, val: i64) -> Self {
544        self.min(val)
545    }
546
547    /// Less than (exclusive).
548    pub fn lt(mut self, val: i64) -> Self {
549        self.inner.checks.push(NumberCheck::Lt(
550            val as f64,
551            format!("Number must be less than {}", val),
552        ));
553        self
554    }
555
556    /// Less than or equal (inclusive). Same as `max`.
557    pub fn lte(self, val: i64) -> Self {
558        self.max(val)
559    }
560
561    /// Must be positive (> 0).
562    pub fn positive(mut self) -> Self {
563        self.inner = self.inner.positive();
564        self
565    }
566
567    /// Must be negative (< 0).
568    pub fn negative(mut self) -> Self {
569        self.inner = self.inner.negative();
570        self
571    }
572
573    /// Must be non-negative (>= 0).
574    pub fn non_negative(mut self) -> Self {
575        self.inner = self.inner.non_negative();
576        self
577    }
578
579    /// Must be non-positive (<= 0).
580    pub fn non_positive(mut self) -> Self {
581        self.inner = self.inner.non_positive();
582        self
583    }
584
585    /// Must be within JavaScript's safe integer range.
586    pub fn safe(mut self) -> Self {
587        self.inner = self.inner.safe();
588        self
589    }
590
591    /// Must be a multiple of the given value.
592    pub fn multiple_of(mut self, val: i64) -> Self {
593        self.inner.checks.push(NumberCheck::MultipleOf(
594            val as f64,
595            format!("Number must be a multiple of {}", val),
596        ));
597        self
598    }
599}
600
601impl ZInt {
602    /// Generate a JSON Schema representation of this integer schema.
603    ///
604    /// Requires the `openapi` feature.
605    #[cfg(feature = "openapi")]
606    pub fn to_json_schema(&self) -> serde_json::Value {
607        let mut schema = self.inner.to_json_schema();
608        schema["type"] = serde_json::json!("integer");
609        schema
610    }
611}
612
613impl VldSchema for ZInt {
614    type Output = i64;
615
616    fn parse_value(&self, value: &Value) -> Result<i64, VldError> {
617        let n = self.inner.extract_number(value)?;
618
619        // Check integer
620        if n.fract() != 0.0 {
621            let msg = self
622                .custom_int_error
623                .clone()
624                .unwrap_or_else(|| "Expected integer, received float".to_string());
625            return Err(VldError::single_with_value(IssueCode::NotInt, msg, value));
626        }
627
628        // Run other number checks
629        self.inner.validate_number(n, value)?;
630
631        Ok(n as i64)
632    }
633}