Skip to main content

tjson/
number.rs

1use std::fmt;
2use std::str::FromStr;
3
4/// A JSON number value, stored as its original string representation.
5///
6/// Validation is delegated to `serde_json`'s number parser, so any string accepted here
7/// is guaranteed to be a valid JSON number. NaN and infinity are rejected.
8///
9/// # Construction
10///
11/// ```
12/// use tjson::Number;
13///
14/// let n: Number = "42".parse().unwrap();
15/// let n: Number = "-3.14".parse().unwrap();
16/// let n: Number = "1e100".parse().unwrap();
17///
18/// assert!(Number::try_from(f64::NAN).is_err());
19/// assert!(Number::try_from(f64::INFINITY).is_err());
20/// ```
21#[derive(Clone, Debug, PartialEq, Eq, Hash)]
22pub struct Number(pub(crate) String);
23
24/// Error returned when a value is not a finite, valid JSON number.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct InvalidNumber(String);
27
28impl fmt::Display for InvalidNumber {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        write!(f, "invalid JSON number: {}", self.0)
31    }
32}
33
34impl std::error::Error for InvalidNumber {}
35
36impl Number {
37    /// Returns the number as its string representation.
38    pub fn as_str(&self) -> &str {
39        &self.0
40    }
41
42    /// Returns the value as an `i64` if it is an integer that fits.
43    pub fn as_i64(&self) -> Option<i64> {
44        self.0.parse().ok()
45    }
46
47    /// Returns the value as a `u64` if it is a non-negative integer that fits.
48    pub fn as_u64(&self) -> Option<u64> {
49        self.0.parse().ok()
50    }
51
52    /// Returns the value as an `f64`.
53    ///
54    /// Returns `None` only if the string somehow fails to parse as a float, which cannot
55    /// happen for any `Number` constructed through the public API. Large integers and
56    /// high-precision decimals may lose precision in the conversion.
57    pub fn as_f64(&self) -> Option<f64> {
58        self.0.parse().ok()
59    }
60
61    /// Returns `true` if the number has no fractional or exponent part.
62    pub fn is_integer(&self) -> bool {
63        !self.0.contains('.') && !self.0.contains('e') && !self.0.contains('E')
64    }
65
66    /// Convert to a `serde_json::Number`. The string was validated by `serde_json`'s own
67    /// parser at construction, so this parse cannot fail.
68    pub(crate) fn to_serde_json_number(&self) -> serde_json::Number {
69        self.0.parse().expect("Number string validated by serde_json at construction")
70    }
71}
72
73impl FromStr for Number {
74    type Err = InvalidNumber;
75
76    fn from_str(s: &str) -> Result<Self, Self::Err> {
77        // Use serde_json for validation. We store the original string, not the
78        // serde_json representation, to preserve exact round-trip fidelity.
79        s.parse::<serde_json::Number>()
80            .map(|_| Self(s.to_owned()))
81            .map_err(|_| InvalidNumber(s.to_owned()))
82    }
83}
84
85impl TryFrom<f64> for Number {
86    type Error = InvalidNumber;
87
88    fn try_from(value: f64) -> Result<Self, Self::Error> {
89        // from_f64 returns None for NaN and infinity.
90        serde_json::Number::from_f64(value)
91            .map(|n| Self(n.to_string()))
92            .ok_or_else(|| InvalidNumber(value.to_string()))
93    }
94}
95
96impl From<i64> for Number {
97    fn from(value: i64) -> Self { Self(value.to_string()) }
98}
99
100impl From<u64> for Number {
101    fn from(value: u64) -> Self { Self(value.to_string()) }
102}
103
104impl From<i32> for Number {
105    fn from(value: i32) -> Self { Self(value.to_string()) }
106}
107
108impl From<u32> for Number {
109    fn from(value: u32) -> Self { Self(value.to_string()) }
110}
111
112impl fmt::Display for Number {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        f.write_str(&self.0)
115    }
116}
117
118impl serde::Serialize for Number {
119    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
120        self.to_serde_json_number().serialize(serializer)
121    }
122}
123
124impl<'de> serde::Deserialize<'de> for Number {
125    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
126        serde_json::Number::deserialize(deserializer).map(|n| Self(n.to_string()))
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn parse_valid() {
136        for s in ["0", "-0", "1", "-1", "42", "3.14", "-3.14", "1e10", "1E10",
137                  "1.5e-3", "1.5E+3", "0.0", "99999999999999999999"] {
138            assert!(s.parse::<Number>().is_ok(), "expected valid: {s}");
139        }
140    }
141
142    #[test]
143    fn parse_invalid() {
144        for s in ["", "nan", "NaN", "inf", "Infinity", "-inf",
145                  "1.", ".5", "1e", "1e+", "01", "--1", "+1"] {
146            assert!(s.parse::<Number>().is_err(), "expected invalid: {s}");
147        }
148    }
149
150    #[test]
151    fn roundtrip_string() {
152        for s in ["42", "-3.14", "1e100", "1E10", "99999999999999999999"] {
153            let n: Number = s.parse().unwrap();
154            assert_eq!(n.as_str(), s, "roundtrip failed for {s}");
155        }
156    }
157
158    #[test]
159    fn from_f64_rejects_non_finite() {
160        assert!(Number::try_from(f64::NAN).is_err());
161        assert!(Number::try_from(f64::INFINITY).is_err());
162        assert!(Number::try_from(f64::NEG_INFINITY).is_err());
163    }
164
165    #[test]
166    fn from_f64_finite() {
167        let n = Number::try_from(3.14_f64).unwrap();
168        assert_eq!(n.as_str(), "3.14");
169    }
170
171    #[test]
172    fn from_integers() {
173        assert_eq!(Number::from(42i64).as_str(), "42");
174        assert_eq!(Number::from(u64::MAX).as_str(), "18446744073709551615");
175        assert_eq!(Number::from(-1i64).as_str(), "-1");
176    }
177
178    #[test]
179    fn as_accessors() {
180        let n: Number = "42".parse().unwrap();
181        assert_eq!(n.as_i64(), Some(42));
182        assert_eq!(n.as_u64(), Some(42));
183
184        let n: Number = "-5".parse().unwrap();
185        assert_eq!(n.as_i64(), Some(-5));
186        assert_eq!(n.as_u64(), None);
187
188        let n: Number = "3.14".parse().unwrap();
189        assert_eq!(n.as_i64(), None);
190        assert!((n.as_f64().unwrap() - 3.14).abs() < 1e-10);
191    }
192
193    #[test]
194    fn is_integer() {
195        assert!("42".parse::<Number>().unwrap().is_integer());
196        assert!(!"3.14".parse::<Number>().unwrap().is_integer());
197        assert!(!"1e10".parse::<Number>().unwrap().is_integer());
198    }
199}