Skip to main content

devicerail_protocol/
wire_integer.rs

1use serde::{Deserialize, Deserializer, Serializer};
2use serde_json::Value;
3
4/// Largest integer that round-trips through JavaScript and JSON `number`
5/// implementations without loss.
6pub const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
7
8/// Interprets a JSON number using JSON Schema's mathematical `integer`
9/// semantics and converts it to `u32` when it is finite and in range.
10///
11/// JSON Schema treats `1`, `1.0`, and `1e0` as the same integer instance.
12/// `serde_json` deliberately keeps the latter representations as floating
13/// point numbers, so relying only on [`Value::as_u64`] would make a Driver's
14/// runtime parser narrower than its advertised schema.
15pub fn json_integer_as_u32(value: &Value) -> Option<u32> {
16    if let Some(integer) = value.as_u64() {
17        return u32::try_from(integer).ok();
18    }
19    let float = value.as_f64()?;
20    if float.is_finite() && float >= 0.0 && float <= f64::from(u32::MAX) && float.fract() == 0.0 {
21        let integer = float as u32;
22        (f64::from(integer) == float).then_some(integer)
23    } else {
24        None
25    }
26}
27
28/// Interprets a JSON number using JSON Schema's mathematical `integer`
29/// semantics and converts it to `i32` when it is finite and in range.
30pub fn json_integer_as_i32(value: &Value) -> Option<i32> {
31    if let Some(integer) = value.as_i64() {
32        return i32::try_from(integer).ok();
33    }
34    if let Some(integer) = value.as_u64() {
35        return i32::try_from(integer).ok();
36    }
37    let float = value.as_f64()?;
38    if float.is_finite()
39        && float >= f64::from(i32::MIN)
40        && float <= f64::from(i32::MAX)
41        && float.fract() == 0.0
42    {
43        let integer = float as i32;
44        (f64::from(integer) == float).then_some(integer)
45    } else {
46        None
47    }
48}
49
50pub fn serialize_js_safe_u64<S>(value: &u64, serializer: S) -> Result<S::Ok, S::Error>
51where
52    S: Serializer,
53{
54    if *value <= MAX_SAFE_INTEGER {
55        serializer.serialize_u64(*value)
56    } else {
57        Err(serde::ser::Error::custom(format!(
58            "integer exceeds the cross-language safe limit {MAX_SAFE_INTEGER}"
59        )))
60    }
61}
62
63pub fn deserialize_js_safe_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
64where
65    D: Deserializer<'de>,
66{
67    let value = u64::deserialize(deserializer)?;
68    if value <= MAX_SAFE_INTEGER {
69        Ok(value)
70    } else {
71        Err(serde::de::Error::custom(format!(
72            "integer exceeds the cross-language safe limit {MAX_SAFE_INTEGER}"
73        )))
74    }
75}
76
77pub fn serialize_optional_js_safe_u64<S>(
78    value: &Option<u64>,
79    serializer: S,
80) -> Result<S::Ok, S::Error>
81where
82    S: Serializer,
83{
84    match value {
85        Some(value) if *value <= MAX_SAFE_INTEGER => serializer.serialize_some(value),
86        Some(_) => Err(serde::ser::Error::custom(format!(
87            "integer exceeds the cross-language safe limit {MAX_SAFE_INTEGER}"
88        ))),
89        None => serializer.serialize_none(),
90    }
91}
92
93pub fn deserialize_optional_js_safe_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
94where
95    D: Deserializer<'de>,
96{
97    let value = Option::<u64>::deserialize(deserializer)?;
98    match value {
99        Some(value) if value > MAX_SAFE_INTEGER => Err(serde::de::Error::custom(format!(
100            "integer exceeds the cross-language safe limit {MAX_SAFE_INTEGER}"
101        ))),
102        value => Ok(value),
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use serde::{Deserialize, Serialize};
109    use serde_json::{Value, json};
110
111    use super::{MAX_SAFE_INTEGER, json_integer_as_i32, json_integer_as_u32};
112
113    #[derive(Debug, Deserialize, Serialize)]
114    struct WireInteger {
115        #[serde(
116            serialize_with = "super::serialize_js_safe_u64",
117            deserialize_with = "super::deserialize_js_safe_u64"
118        )]
119        value: u64,
120    }
121
122    #[test]
123    fn wire_integer_accepts_the_limit_and_rejects_larger_values() {
124        let valid: WireInteger =
125            serde_json::from_value(json!({ "value": MAX_SAFE_INTEGER })).expect("safe integer");
126        assert_eq!(valid.value, MAX_SAFE_INTEGER);
127        assert!(
128            serde_json::from_value::<WireInteger>(json!({ "value": MAX_SAFE_INTEGER + 1 }))
129                .is_err()
130        );
131        assert!(
132            serde_json::to_value(WireInteger {
133                value: MAX_SAFE_INTEGER + 1,
134            })
135            .is_err()
136        );
137    }
138
139    #[test]
140    fn json_schema_integer_forms_convert_without_widening_the_range() {
141        for representation in ["1", "1.0", "1e0", "-0.0"] {
142            let value: Value = serde_json::from_str(representation).expect("JSON number");
143            assert_eq!(
144                json_integer_as_u32(&value),
145                Some(if representation == "-0.0" { 0 } else { 1 })
146            );
147        }
148        for representation in ["-1", "-1.0", "-1e0"] {
149            let value: Value = serde_json::from_str(representation).expect("JSON number");
150            assert_eq!(json_integer_as_i32(&value), Some(-1));
151        }
152
153        for representation in ["1.5", "-1", "4294967296", "4294967296.0"] {
154            let value: Value = serde_json::from_str(representation).expect("JSON number");
155            assert_eq!(json_integer_as_u32(&value), None);
156        }
157        for representation in ["1.5", "2147483648", "-2147483649", "2147483648.0"] {
158            let value: Value = serde_json::from_str(representation).expect("JSON number");
159            assert_eq!(json_integer_as_i32(&value), None);
160        }
161        assert_eq!(json_integer_as_u32(&json!("1")), None);
162        assert_eq!(json_integer_as_i32(&Value::Null), None);
163    }
164}