Skip to main content

salvo_oapi/openapi/schema/
number.rs

1use serde::de::{self, Visitor};
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3
4/// Represents a numeric value in OpenAPI schema validation fields.
5///
6/// This type preserves the distinction between integers and floating-point numbers,
7/// ensuring that integer values like `1` serialize as `1` rather than `1.0` in JSON output.
8///
9/// # Examples
10///
11/// ```
12/// # use salvo_oapi::schema::Number;
13/// let int_val: Number = 42.into();
14/// let float_val: Number = 3.14.into();
15///
16/// assert_eq!(serde_json::to_string(&int_val).unwrap(), "42");
17/// assert_eq!(serde_json::to_string(&float_val).unwrap(), "3.14");
18/// ```
19#[derive(Clone, Debug)]
20pub enum Number {
21    /// Signed integer value e.g. `1` or `-2`.
22    Int(isize),
23    /// Unsigned integer value e.g. `0`.
24    UInt(usize),
25    /// Floating point number e.g. `1.34`.
26    Float(f64),
27}
28
29impl Eq for Number {}
30
31impl PartialEq for Number {
32    fn eq(&self, other: &Self) -> bool {
33        match (self, other) {
34            (Self::Int(left), Self::Int(right)) => left == right,
35            (Self::UInt(left), Self::UInt(right)) => left == right,
36            (Self::Float(left), Self::Float(right)) => left == right,
37            _ => false,
38        }
39    }
40}
41
42impl Serialize for Number {
43    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
44    where
45        S: Serializer,
46    {
47        match self {
48            Self::Int(value) => serializer.serialize_i64(*value as i64),
49            Self::UInt(value) => serializer.serialize_u64(*value as u64),
50            Self::Float(value) => {
51                // Serialize whole floats as integers to avoid trailing `.0`
52                if value.fract() == 0.0 && value.is_finite() {
53                    if *value < 0.0 {
54                        serializer.serialize_i64(*value as i64)
55                    } else {
56                        serializer.serialize_u64(*value as u64)
57                    }
58                } else {
59                    serializer.serialize_f64(*value)
60                }
61            }
62        }
63    }
64}
65
66struct NumberVisitor;
67
68impl<'de> Visitor<'de> for NumberVisitor {
69    type Value = Number;
70
71    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
72        formatter.write_str("a number (integer or float)")
73    }
74
75    fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
76    where
77        E: de::Error,
78    {
79        Ok(Number::Int(v as isize))
80    }
81
82    fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
83    where
84        E: de::Error,
85    {
86        Ok(Number::UInt(v as usize))
87    }
88
89    fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
90    where
91        E: de::Error,
92    {
93        Ok(Number::Float(v))
94    }
95}
96
97impl<'de> Deserialize<'de> for Number {
98    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
99    where
100        D: Deserializer<'de>,
101    {
102        deserializer.deserialize_any(NumberVisitor)
103    }
104}
105
106macro_rules! impl_from_for_number {
107    ( $( $ty:ident => $pat:ident $( as $as:ident )? ),* ) => {
108        $(
109        impl From<$ty> for Number {
110            fn from(value: $ty) -> Self {
111                Self::$pat(value $( as $as )?)
112            }
113        }
114        )*
115    };
116}
117
118#[rustfmt::skip]
119impl_from_for_number!(
120    f32 => Float as f64, f64 => Float,
121    i8 => Int as isize, i16 => Int as isize, i32 => Int as isize, i64 => Int as isize,
122    u8 => UInt as usize, u16 => UInt as usize, u32 => UInt as usize, u64 => UInt as usize,
123    isize => Int, usize => UInt
124);
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_serialize_int() {
132        let n = Number::Int(42);
133        assert_eq!(serde_json::to_string(&n).unwrap(), "42");
134    }
135
136    #[test]
137    fn test_serialize_negative_int() {
138        let n = Number::Int(-5);
139        assert_eq!(serde_json::to_string(&n).unwrap(), "-5");
140    }
141
142    #[test]
143    fn test_serialize_uint() {
144        let n = Number::UInt(100);
145        assert_eq!(serde_json::to_string(&n).unwrap(), "100");
146    }
147
148    #[test]
149    #[allow(clippy::approx_constant)]
150    fn test_serialize_float() {
151        let n = Number::Float(3.14);
152        assert_eq!(serde_json::to_string(&n).unwrap(), "3.14");
153    }
154
155    #[test]
156    fn test_serialize_whole_float_as_integer() {
157        let n = Number::Float(10.0);
158        assert_eq!(serde_json::to_string(&n).unwrap(), "10");
159    }
160
161    #[test]
162    fn test_serialize_negative_whole_float() {
163        let n = Number::Float(-3.0);
164        assert_eq!(serde_json::to_string(&n).unwrap(), "-3");
165    }
166
167    #[test]
168    fn test_from_i32() {
169        let n: Number = 42i32.into();
170        assert_eq!(n, Number::Int(42));
171    }
172
173    #[test]
174    fn test_from_u64() {
175        let n: Number = 100u64.into();
176        assert_eq!(n, Number::UInt(100));
177    }
178
179    #[test]
180    fn test_from_f64() {
181        let n: Number = 2.5f64.into();
182        assert_eq!(n, Number::Float(2.5));
183    }
184
185    #[test]
186    fn test_deserialize_int() {
187        let n: Number = serde_json::from_str("42").unwrap();
188        assert_eq!(n, Number::UInt(42));
189    }
190
191    #[test]
192    fn test_deserialize_negative_int() {
193        let n: Number = serde_json::from_str("-5").unwrap();
194        assert_eq!(n, Number::Int(-5));
195    }
196
197    #[test]
198    #[allow(clippy::approx_constant)]
199    fn test_deserialize_float() {
200        let n: Number = serde_json::from_str("3.14").unwrap();
201        assert_eq!(n, Number::Float(3.14));
202    }
203
204    #[test]
205    fn test_equality() {
206        assert_eq!(Number::Int(1), Number::Int(1));
207        assert_eq!(Number::UInt(1), Number::UInt(1));
208        assert_eq!(Number::Float(1.5), Number::Float(1.5));
209        assert_ne!(Number::Int(1), Number::UInt(1));
210    }
211}