Skip to main content

vld/primitives/number/
f32.rs

1use serde_json::Value;
2
3use crate::error::{IssueCode, SizeOrigin, VldError};
4use crate::schema::VldSchema;
5
6use super::{Num, ZNumber};
7
8/// Schema for `f32` numbers. Created via [`vld::f32()`](crate::f32).
9///
10/// Values outside the finite `f32` range fail with `too_small` / `too_big`
11/// (origin `number`). Bounds and the input are compared at `f32` precision
12/// (via their shortest decimal form), so `vld::f32().max(0.1)` accepts `0.1`,
13/// `multiple_of(0.1)` accepts `0.3`, and messages print `0.1`.
14///
15/// JSON Schema: `{"type": "number", "format": "float"}` plus bounds.
16///
17/// # Example
18/// ```
19/// use vld::prelude::*;
20///
21/// let ratio: f32 = vld::f32().min(0.0).max(1.0).parse("0.25").unwrap();
22/// assert_eq!(ratio, 0.25);
23/// assert!(vld::f32().parse("1e39").is_err()); // above f32::MAX
24/// ```
25#[derive(Clone, Default)]
26pub struct ZF32 {
27    inner: ZNumber,
28}
29
30/// The `f64` nearest to the shortest decimal form of `v` (`0.1f32` → `0.1`,
31/// not `0.10000000149011612`). Bounds and inputs are both widened this way,
32/// so comparisons, `multiple_of` and messages work at `f32` precision.
33fn widen(v: f32) -> f64 {
34    v.to_string().parse().unwrap_or(f64::from(v))
35}
36
37macro_rules! bound_methods {
38    ($($(#[$doc:meta])* $name:ident),+ $(,)?) => {$(
39        $(#[$doc])*
40        pub fn $name(mut self, val: f32) -> Self {
41            self.inner = self.inner.$name(widen(val));
42            self
43        }
44    )+};
45}
46
47macro_rules! flag_methods {
48    ($($(#[$doc:meta])* $name:ident),+ $(,)?) => {$(
49        $(#[$doc])*
50        pub fn $name(mut self) -> Self {
51            self.inner = self.inner.$name();
52            self
53        }
54    )+};
55}
56
57impl ZF32 {
58    /// Create the schema (the same as its `vld::` constructor function).
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Set a custom error message for type mismatch (non-number input).
64    pub fn type_error(mut self, msg: impl Into<String>) -> Self {
65        self.inner = self.inner.type_error(msg);
66        self
67    }
68
69    /// Override error messages in bulk by check key (see [`ZNumber::with_messages`]).
70    pub fn with_messages<F>(mut self, f: F) -> Self
71    where
72        F: Fn(&str) -> Option<String> + Send + Sync + 'static,
73    {
74        self.inner = self.inner.with_messages(f);
75        self
76    }
77
78    bound_methods! {
79        /// Minimum value (inclusive).
80        min,
81        /// Maximum value (inclusive).
82        max,
83        /// Greater than (exclusive).
84        gt,
85        /// Greater than or equal (inclusive). Same as `min`.
86        gte,
87        /// Less than (exclusive).
88        lt,
89        /// Less than or equal (inclusive). Same as `max`.
90        lte,
91        /// Must be a multiple of the given value (`0` never matches).
92        multiple_of,
93    }
94
95    flag_methods! {
96        /// Must be positive (> 0).
97        positive,
98        /// Must be negative (< 0).
99        negative,
100        /// Must be non-negative (>= 0).
101        non_negative,
102        /// Must be non-positive (<= 0).
103        non_positive,
104        /// Coerce strings and booleans to numbers.
105        coerce,
106    }
107}
108
109impl VldSchema for ZF32 {
110    type Output = f32;
111
112    fn parse_value(&self, value: &Value) -> Result<f32, VldError> {
113        let f = self.inner.extract_number(value)?;
114        let out = f as f32;
115        // Rounding to f32 overflows to infinity only past the finite range
116        // (values within half an ulp of f32::MAX round down to it).
117        let limit = f64::from(f32::MAX);
118        if out == f32::NEG_INFINITY {
119            return Err(VldError::from_code_with_value(
120                IssueCode::TooSmall {
121                    origin: SizeOrigin::Number,
122                    minimum: (-limit).into(),
123                    inclusive: true,
124                },
125                value,
126            ));
127        }
128        if out == f32::INFINITY {
129            return Err(VldError::from_code_with_value(
130                IssueCode::TooBig {
131                    origin: SizeOrigin::Number,
132                    maximum: limit.into(),
133                    inclusive: true,
134                },
135                value,
136            ));
137        }
138        self.inner.validate(Num::F(widen(out)), value)?;
139        Ok(out)
140    }
141}
142
143#[cfg(feature = "openapi")]
144impl crate::openapi::JsonSchema for ZF32 {
145    fn json_schema(&self) -> serde_json::Value {
146        // The f32 range is part of the type (like the sized integers' range).
147        let max = Num::F(f64::from(f32::MAX));
148        let min = Num::F(f64::from(f32::MIN));
149        let mut schema = self.inner.schema("number", Some((min, max)));
150        schema["format"] = serde_json::json!("float");
151        schema
152    }
153}
154
155#[cfg(feature = "openapi")]
156impl crate::openapi::CollectNestedSchemas for ZF32 {}
157
158#[cfg(all(test, feature = "openapi"))]
159mod schema_tests {
160    use crate::openapi::JsonSchema;
161
162    #[test]
163    fn f32_schema_carries_the_type_range() {
164        let s = crate::f32().json_schema();
165        assert_eq!(s["minimum"], serde_json::json!(f64::from(f32::MIN)));
166        assert_eq!(s["maximum"], serde_json::json!(f64::from(f32::MAX)));
167        let s = crate::f32().min(0.5).json_schema();
168        assert_eq!(s["minimum"], serde_json::json!(0.5));
169    }
170}