Skip to main content

vld/primitives/number/
float.rs

1use serde_json::Value;
2#[cfg(feature = "openapi")]
3use std::cmp::Ordering;
4
5use crate::error::{IssueCode, ValidationIssue, VldError, issue_error};
6use crate::schema::VldSchema;
7
8use super::{Kind, Num, NumberCheck, ZInt};
9
10/// Schema for number validation (`f64`). Created via [`vld::number()`](crate::number).
11///
12/// Use `.int()` to convert to integer validation (`i64`).
13///
14/// `NaN` never satisfies a bound check, and `.coerce()` rejects strings that
15/// parse to `NaN` or infinity.
16///
17/// # Example
18/// ```
19/// use vld::prelude::*;
20///
21/// let schema = vld::number().min(0.0).max(100.0);
22/// let int_schema = vld::number().int().min(0).max(100);
23/// ```
24#[derive(Clone)]
25pub struct ZNumber {
26    checks: Vec<NumberCheck>,
27    pub(super) coerce: bool,
28    custom_type_error: Option<String>,
29    messages: crate::primitives::Messages,
30}
31
32impl ZNumber {
33    /// Create the schema (the same as its `vld::` constructor function).
34    pub fn new() -> Self {
35        Self {
36            checks: vec![],
37            coerce: false,
38            custom_type_error: None,
39            messages: None,
40        }
41    }
42
43    fn push(mut self, kind: Kind) -> Self {
44        self.checks.push(NumberCheck { kind, msg: None });
45        self
46    }
47
48    /// Set a custom error message for type mismatch (when the input is not a number).
49    ///
50    /// # Example
51    /// ```
52    /// use vld::prelude::*;
53    /// let schema = vld::number().type_error("Must be a number!");
54    /// let err = schema.parse(r#""hello""#).unwrap_err();
55    /// assert!(err.issues[0].message.contains("Must be a number!"));
56    /// ```
57    pub fn type_error(mut self, msg: impl Into<String>) -> Self {
58        self.custom_type_error = Some(msg.into());
59        self
60    }
61
62    /// Override error messages in bulk by check key.
63    ///
64    /// The closure receives the check key (e.g. `"too_small"`, `"too_big"`,
65    /// `"not_positive"`, `"not_multiple_of"`, `"not_safe"`)
66    /// and should return `Some(new_message)` to replace, or `None` to keep the original.
67    ///
68    /// The closure is consulted when an issue is built, so it covers checks
69    /// added before **and after** this call; a per-check message
70    /// (`*_msg(...)`) takes precedence over it. A later call replaces it.
71    ///
72    /// # Example
73    /// ```
74    /// use vld::prelude::*;
75    /// let schema = vld::number().min(0.0).max(100.0)
76    ///     .with_messages(|key| match key {
77    ///         "too_small" => Some("Must be >= 0".into()),
78    ///         "too_big" => Some("Must be <= 100".into()),
79    ///         _ => None,
80    ///     });
81    /// ```
82    pub fn with_messages<F>(mut self, f: F) -> Self
83    where
84        F: Fn(&str) -> Option<String> + Send + Sync + 'static,
85    {
86        self.messages = Some(std::sync::Arc::new(f));
87        self
88    }
89
90    pub(super) fn min_n(self, v: Num) -> Self {
91        self.push(Kind::Min(v))
92    }
93
94    pub(super) fn max_n(self, v: Num) -> Self {
95        self.push(Kind::Max(v))
96    }
97
98    pub(super) fn gt_n(self, v: Num) -> Self {
99        self.push(Kind::Gt(v))
100    }
101
102    pub(super) fn lt_n(self, v: Num) -> Self {
103        self.push(Kind::Lt(v))
104    }
105
106    pub(super) fn multiple_of_n(self, v: Num) -> Self {
107        self.push(Kind::MultipleOf(v))
108    }
109
110    /// Minimum value (inclusive). Alias: `gte`.
111    pub fn min(self, val: f64) -> Self {
112        self.min_n(Num::F(val))
113    }
114
115    /// Maximum value (inclusive). Alias: `lte`.
116    pub fn max(self, val: f64) -> Self {
117        self.max_n(Num::F(val))
118    }
119
120    /// Greater than (exclusive).
121    pub fn gt(self, val: f64) -> Self {
122        self.gt_n(Num::F(val))
123    }
124
125    /// Greater than or equal (inclusive). Same as `min`.
126    pub fn gte(self, val: f64) -> Self {
127        self.min(val)
128    }
129
130    /// Less than (exclusive).
131    pub fn lt(self, val: f64) -> Self {
132        self.lt_n(Num::F(val))
133    }
134
135    /// Less than or equal (inclusive). Same as `max`.
136    pub fn lte(self, val: f64) -> Self {
137        self.max(val)
138    }
139
140    /// Must be positive (> 0).
141    pub fn positive(self) -> Self {
142        self.push(Kind::Positive)
143    }
144
145    /// Must be negative (< 0).
146    pub fn negative(self) -> Self {
147        self.push(Kind::Negative)
148    }
149
150    /// Must be non-negative (>= 0).
151    pub fn non_negative(self) -> Self {
152        self.push(Kind::NonNegative)
153    }
154
155    /// Must be non-positive (<= 0).
156    pub fn non_positive(self) -> Self {
157        self.push(Kind::NonPositive)
158    }
159
160    /// Must be a multiple of the given value.
161    ///
162    /// Uses a relative tolerance of `1e-9` so decimal steps like `0.1` work.
163    /// A divisor of `0` never matches (every value is rejected).
164    pub fn multiple_of(self, val: f64) -> Self {
165        self.multiple_of_n(Num::F(val))
166    }
167
168    /// Must be within JavaScript's safe integer range (`-(2^53 - 1)` to `2^53 - 1`).
169    pub fn safe(self) -> Self {
170        self.push(Kind::Safe)
171    }
172
173    /// Convert to integer validation. Returns `ZInt` with `Output = i64`.
174    pub fn int(self) -> ZInt {
175        ZInt::from_number(self)
176    }
177
178    /// Coerce strings and booleans to numbers.
179    ///
180    /// Strings that parse to `NaN` or infinity are rejected.
181    pub fn coerce(mut self) -> Self {
182        self.coerce = true;
183        self
184    }
185
186    fn type_err(&self, value: &Value) -> VldError {
187        crate::primitives::type_error(&self.custom_type_error, "number", value)
188    }
189
190    fn coerce_err(&self, value: &Value) -> VldError {
191        issue_error(
192            IssueCode::InvalidType {
193                expected: "number".to_string(),
194                received: "string".to_string(),
195            },
196            self.custom_type_error.as_ref(),
197            value,
198        )
199    }
200
201    pub(super) fn extract_number(&self, value: &Value) -> Result<f64, VldError> {
202        match value {
203            Value::Number(n) => n.as_f64().ok_or_else(|| self.type_err(value)),
204            Value::String(s) if self.coerce => s
205                .parse::<f64>()
206                .ok()
207                .filter(|f| f.is_finite())
208                .ok_or_else(|| self.coerce_err(value)),
209            Value::Bool(b) if self.coerce => Ok(if *b { 1.0 } else { 0.0 }),
210            _ => Err(self.type_err(value)),
211        }
212    }
213
214    pub(super) fn validate(&self, n: Num, value: &Value) -> Result<(), VldError> {
215        let mut errors = VldError::new();
216        for check in &self.checks {
217            if let Some(code) = check.kind.failure(n) {
218                let bulk = || self.messages.as_ref().and_then(|f| f(check.kind.key()));
219                let msg = check.msg.clone().or_else(bulk);
220                errors
221                    .issues
222                    .push(ValidationIssue::with_override(code, msg.as_ref()).with_received(value));
223            }
224        }
225        if errors.is_empty() {
226            Ok(())
227        } else {
228            Err(errors)
229        }
230    }
231}
232
233impl Default for ZNumber {
234    fn default() -> Self {
235        Self::new()
236    }
237}
238
239impl VldSchema for ZNumber {
240    type Output = f64;
241
242    fn parse_value(&self, value: &Value) -> Result<f64, VldError> {
243        let n = self.extract_number(value)?;
244        self.validate(Num::F(n), value)?;
245        Ok(n)
246    }
247}
248
249/// Generate a JSON Schema representation of this number schema.
250///
251/// The strictest lower/upper bound (from `min`/`gt`/`positive`/
252/// `non_negative`/`safe`, …) is emitted as `minimum` or `exclusiveMinimum`
253/// (likewise for the maximum). The first `multiple_of` goes into
254/// `multipleOf`, further ones into `allOf`.
255#[cfg(feature = "openapi")]
256impl crate::openapi::JsonSchema for ZNumber {
257    fn json_schema(&self) -> serde_json::Value {
258        self.schema("number", None)
259    }
260}
261
262impl ZNumber {
263    /// JSON Schema of the checks; `range` is the output type's own
264    /// `[min, max]`, intersected with the user bounds.
265    #[cfg(feature = "openapi")]
266    pub(super) fn schema(&self, ty: &str, range: Option<(Num, Num)>) -> Value {
267        let mut schema = serde_json::json!({ "type": ty });
268        // Strictest bound: larger lower bound wins, exclusive wins ties.
269        let pick = |a: (Num, bool), b: (Num, bool), want: Ordering| match b.0.cmp(a.0) {
270            Some(o) if o == want => b,
271            Some(Ordering::Equal) if b.1 => b,
272            _ => a,
273        };
274        let lower = range
275            .map(|(lo, _)| (lo, false))
276            .into_iter()
277            .chain(self.checks.iter().filter_map(|c| c.kind.lower()))
278            .reduce(|a, b| pick(a, b, Ordering::Greater));
279        let upper = range
280            .map(|(_, hi)| (hi, false))
281            .into_iter()
282            .chain(self.checks.iter().filter_map(|c| c.kind.upper()))
283            .reduce(|a, b| pick(a, b, Ordering::Less));
284        if let Some((v, excl)) = lower {
285            let key = if excl { "exclusiveMinimum" } else { "minimum" };
286            schema[key] = v.json();
287        }
288        if let Some((v, excl)) = upper {
289            let key = if excl { "exclusiveMaximum" } else { "maximum" };
290            schema[key] = v.json();
291        }
292        // `multipleOf` must be > 0: a zero divisor (never matches) is omitted.
293        let mut multiples = self.checks.iter().filter_map(|c| match c.kind {
294            Kind::MultipleOf(v) if v.as_f64() != 0.0 => Some(v.json()),
295            _ => None,
296        });
297        if let Some(m) = multiples.next() {
298            schema["multipleOf"] = m;
299        }
300        let rest: Vec<Value> = multiples
301            .map(|m| serde_json::json!({ "multipleOf": m }))
302            .collect();
303        if !rest.is_empty() {
304            schema["allOf"] = Value::Array(rest);
305        }
306        schema
307    }
308}
309
310#[cfg(feature = "openapi")]
311impl crate::openapi::CollectNestedSchemas for ZNumber {}