Skip to main content

yapi_types/
json.rs

1//! JSON output that matches pi's `JSON.stringify`.
2//!
3//! serde_json and JavaScript escape strings the same way but print numbers
4//! differently: JavaScript writes `0.000003` and `5` where serde_json writes
5//! `3e-6` and `5.0`. These functions print numbers the JavaScript way, so a
6//! document parsed from a pi file serializes back to the same bytes.
7
8use std::io;
9
10use serde::Serialize;
11use serde::de::DeserializeOwned;
12use serde_json::ser::{CompactFormatter, Formatter, PrettyFormatter, Serializer};
13use serde_json::{Map, Value};
14
15/// Serializes `value` like `JSON.stringify(value)`.
16pub fn to_string<T: Serialize + ?Sized>(value: &T) -> serde_json::Result<String> {
17    write(value, JsFormatter(CompactFormatter))
18}
19
20/// A JSON document in memory, whose serialization cannot fail.
21pub trait JsonValue: Serialize {}
22
23impl JsonValue for Value {}
24
25impl JsonValue for Map<String, Value> {}
26
27/// Serializes a JSON value like `JSON.stringify(value)`.
28pub fn stringify(value: &impl JsonValue) -> String {
29    // Object keys are strings and numbers are finite, so serialization succeeds.
30    to_string(value).unwrap_or_default()
31}
32
33/// A JavaScript number as JSON: an integer when it is a safe one, so it
34/// compares equal to the same number parsed from a pi file.
35pub fn number(value: f64) -> Value {
36    if value.fract() == 0.0 && value.abs() <= MAX_SAFE_INTEGER as f64 {
37        Value::from(value as i64)
38    } else {
39        serde_json::Number::from_f64(value).map_or(Value::Null, Value::Number)
40    }
41}
42
43/// JavaScript's `Number(text)`: surrounding whitespace ignored, `0` for
44/// empty text, unsigned `0x`, `0o` and `0b` integers, `Infinity` with an
45/// optional sign, else a decimal literal, and `NaN` for anything else.
46pub fn js_number(text: &str) -> f64 {
47    let text = text.trim_matches(|c: char| c.is_whitespace() || c == '\u{feff}');
48    if text.is_empty() {
49        return 0.0;
50    }
51    for (prefix, radix) in [("0x", 16), ("0o", 8), ("0b", 2)] {
52        if let Some(digits) = text
53            .get(..2)
54            .filter(|start| start.eq_ignore_ascii_case(prefix))
55            .and_then(|_| text.get(2..))
56        {
57            let value = digits.chars().try_fold(0.0_f64, |value, c| {
58                c.to_digit(radix)
59                    .map(|digit| value * f64::from(radix) + f64::from(digit))
60            });
61            return value.filter(|_| !digits.is_empty()).unwrap_or(f64::NAN);
62        }
63    }
64    match text {
65        "Infinity" | "+Infinity" => return f64::INFINITY,
66        "-Infinity" => return f64::NEG_INFINITY,
67        _ => {}
68    }
69    // Rust also reads `inf` and `NaN`, which JavaScript does not.
70    if !text
71        .bytes()
72        .all(|byte| byte.is_ascii_digit() || b"+-.eE".contains(&byte))
73    {
74        return f64::NAN;
75    }
76    text.parse().unwrap_or(f64::NAN)
77}
78
79/// Serializes `value` like `JSON.stringify(value, null, indent)`.
80pub fn to_string_pretty<T: Serialize + ?Sized>(
81    value: &T,
82    indent: &str,
83) -> serde_json::Result<String> {
84    write(
85        value,
86        JsFormatter(PrettyFormatter::with_indent(indent.as_bytes())),
87    )
88}
89
90fn write<T: Serialize + ?Sized>(
91    value: &T,
92    formatter: impl Formatter,
93) -> serde_json::Result<String> {
94    let mut out = Vec::new();
95    value.serialize(&mut Serializer::with_formatter(&mut out, formatter))?;
96    // serde_json only writes UTF-8: string contents come from `str` and escapes are ASCII.
97    Ok(String::from_utf8(out).expect("serde_json writes UTF-8"))
98}
99
100/// Wraps a serde_json formatter, keeping its layout and printing numbers with
101/// ECMAScript `Number.prototype.toString`.
102struct JsFormatter<F>(F);
103
104/// The largest integer a JavaScript number holds exactly.
105const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
106
107impl<F: Formatter> Formatter for JsFormatter<F> {
108    fn write_f64<W: ?Sized + io::Write>(&mut self, writer: &mut W, value: f64) -> io::Result<()> {
109        // serde_json writes `null` for non-finite values before reaching this point.
110        writer.write_all(ryu_js::Buffer::new().format(value).as_bytes())
111    }
112
113    // Integers beyond what a double holds exactly are rounded, as
114    // `JSON.parse` then `JSON.stringify` round them.
115    fn write_i64<W: ?Sized + io::Write>(&mut self, writer: &mut W, value: i64) -> io::Result<()> {
116        if value.unsigned_abs() > MAX_SAFE_INTEGER {
117            return self.write_f64(writer, value as f64);
118        }
119        self.0.write_i64(writer, value)
120    }
121
122    fn write_u64<W: ?Sized + io::Write>(&mut self, writer: &mut W, value: u64) -> io::Result<()> {
123        if value > MAX_SAFE_INTEGER {
124            return self.write_f64(writer, value as f64);
125        }
126        self.0.write_u64(writer, value)
127    }
128
129    fn begin_array<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
130        self.0.begin_array(writer)
131    }
132
133    fn end_array<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
134        self.0.end_array(writer)
135    }
136
137    fn begin_array_value<W: ?Sized + io::Write>(
138        &mut self,
139        writer: &mut W,
140        first: bool,
141    ) -> io::Result<()> {
142        self.0.begin_array_value(writer, first)
143    }
144
145    fn end_array_value<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
146        self.0.end_array_value(writer)
147    }
148
149    fn begin_object<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
150        self.0.begin_object(writer)
151    }
152
153    fn end_object<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
154        self.0.end_object(writer)
155    }
156
157    fn begin_object_key<W: ?Sized + io::Write>(
158        &mut self,
159        writer: &mut W,
160        first: bool,
161    ) -> io::Result<()> {
162        self.0.begin_object_key(writer, first)
163    }
164
165    fn end_object_key<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
166        self.0.end_object_key(writer)
167    }
168
169    fn begin_object_value<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
170        self.0.begin_object_value(writer)
171    }
172
173    fn end_object_value<W: ?Sized + io::Write>(&mut self, writer: &mut W) -> io::Result<()> {
174        self.0.end_object_value(writer)
175    }
176}
177
178/// Parses `text` like pi's `JSON.parse(stripBom(text))`: a leading byte
179/// order mark is ignored.
180pub fn parse<T: DeserializeOwned>(text: &str) -> serde_json::Result<T> {
181    serde_json::from_str(text.strip_prefix('\u{feff}').unwrap_or(text))
182}
183
184#[cfg(test)]
185mod tests {
186    use serde_json::{Value, json};
187
188    use super::*;
189
190    #[test]
191    fn reads_numbers_like_javascript() {
192        let number = |text| super::number(js_number(text));
193        assert_eq!(number("8080"), json!(8080));
194        assert_eq!(number(" 1.5e1 "), json!(15));
195        assert_eq!(number(""), json!(0));
196        assert_eq!(number("x"), Value::Null);
197        assert_eq!(number("0x1F90"), json!(8080));
198        assert_eq!(number(" 0o17 "), json!(15));
199        assert_eq!(number("0B101"), json!(5));
200        for nan in ["0x", "-0x10", "0x+1", "inf", "NaN", "1e", "1_000"] {
201            assert!(js_number(nan).is_nan(), "{nan}");
202        }
203        assert_eq!(js_number("-Infinity"), f64::NEG_INFINITY);
204        assert_eq!(number("Infinity"), Value::Null);
205    }
206
207    #[test]
208    fn parses_past_a_byte_order_mark() {
209        assert_eq!(
210            parse::<Value>("\u{feff}{\"a\":1}").ok(),
211            Some(json!({"a": 1}))
212        );
213        assert_eq!(parse::<Value>("[]").ok(), Some(json!([])));
214        assert!(parse::<Value>("\u{feff}\u{feff}[]").is_err());
215    }
216
217    #[test]
218    fn numbers_match_javascript() {
219        // Expected strings are `String(n)` in JavaScript.
220        let cases: [(f64, &str); 12] = [
221            (0.0, "0"),
222            (-0.0, "0"),
223            (5.0, "5"),
224            (-42.0, "-42"),
225            (0.000003, "0.000003"),
226            (1e-7, "1e-7"),
227            (1e21, "1e+21"),
228            (0.1 + 0.2, "0.30000000000000004"),
229            (9007199254740992.0, "9007199254740992"),
230            (123.456, "123.456"),
231            (5e-324, "5e-324"),
232            (f64::MAX, "1.7976931348623157e+308"),
233        ];
234        for (value, expected) in cases {
235            assert_eq!(to_string(&value).unwrap(), expected, "{value:e}");
236        }
237    }
238
239    #[test]
240    fn large_integers_round_like_javascript() {
241        // `JSON.stringify(JSON.parse(text))` in JavaScript.
242        let value: Value = serde_json::from_str(
243            "[9007199254740991,9007199254740993,12345678901234567890,-12345678901234567]",
244        )
245        .unwrap();
246        assert_eq!(
247            to_string(&value).unwrap(),
248            "[9007199254740991,9007199254740992,12345678901234567000,-12345678901234568]"
249        );
250    }
251
252    #[test]
253    fn parsed_numbers_round_trip() {
254        let text = r#"[0,0.000003,0.0037020000000000004,1e+21,1893456000000,-1.5e-7]"#;
255        let value: Value = serde_json::from_str(text).unwrap();
256        assert_eq!(to_string(&value).unwrap(), text);
257    }
258
259    #[test]
260    fn pretty_matches_javascript_layout() {
261        let value = json!({ "a": [], "b": {}, "c": [1, { "d": null }] });
262        let expected = "{\n  \"a\": [],\n  \"b\": {},\n  \"c\": [\n    1,\n    {\n      \"d\": null\n    }\n  ]\n}";
263        assert_eq!(to_string_pretty(&value, "  ").unwrap(), expected);
264    }
265
266    #[test]
267    fn non_finite_numbers_are_null() {
268        assert_eq!(
269            to_string(&[f64::NAN, f64::INFINITY]).unwrap(),
270            "[null,null]"
271        );
272    }
273}