Skip to main content

doge_runtime/stdlib/
json.rs

1//! `json` — parse and emit JSON. `json.parse(text)` turns a JSON document into a
2//! Doge value (object → Dict, array → List, the rest → Str/Int/Float/Bool/none);
3//! `json.emit(value)` turns one back into compact JSON text. Every malformed input
4//! is a catchable `ValueError` with the offset it failed at, and a value that has
5//! no JSON form (an object, function, socket, …) is a catchable `TypeError` —
6//! neither ever panics.
7
8use std::fmt::Write;
9
10use crate::error::{DogeError, DogeResult};
11use crate::ordered_map::OrderedMap;
12use crate::stdlib::serialize::{escape_str, too_deep, unsupported, MAX_DEPTH};
13use crate::stdlib::str_arg;
14use crate::value::Value;
15
16/// `json.parse(text)` — the value a JSON document denotes. Leading and trailing
17/// whitespace is ignored; anything after the top-level value is an error.
18pub fn json_parse(text: &Value) -> DogeResult {
19    let text = str_arg("json", "parse", text)?;
20    let mut p = Parser {
21        chars: text.chars().collect(),
22        pos: 0,
23    };
24    p.skip_ws();
25    let value = p.value(0)?;
26    p.skip_ws();
27    if p.pos != p.chars.len() {
28        return Err(p.error("expected end of input"));
29    }
30    Ok(value)
31}
32
33/// `json.emit(value)` — a compact JSON document (no insignificant whitespace) for
34/// `value`. Dict/List/Str/Int/Float/Bool/none serialize; any other type, or a
35/// non-finite Float (JSON has no NaN/infinity), is a catchable error.
36pub fn json_emit(value: &Value) -> DogeResult {
37    let mut out = String::new();
38    emit(value, &mut out, 0)?;
39    Ok(Value::str(out))
40}
41
42fn emit(value: &Value, out: &mut String, depth: usize) -> Result<(), DogeError> {
43    if depth >= MAX_DEPTH {
44        return Err(too_deep("json", "emit"));
45    }
46    match value {
47        Value::None => out.push_str("null"),
48        Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
49        Value::Int(n) => {
50            let _ = write!(out, "{n}");
51        }
52        Value::Float(x) => {
53            if !x.is_finite() {
54                return Err(DogeError::value_error(
55                    "json.emit cannot serialize a Float that is not finite",
56                ));
57            }
58            // A whole-number Float keeps its decimal point so it re-parses as a
59            // Float, matching how `bark` prints it (3.0, never 3).
60            if x.fract() == 0.0 {
61                let _ = write!(out, "{x:.1}");
62            } else {
63                let _ = write!(out, "{x}");
64            }
65        }
66        Value::Str(s) => escape_str(s, out, |o, cp| {
67            let _ = write!(o, "\\u{cp:04x}");
68        }),
69        Value::List(items) => {
70            out.push('[');
71            for (i, item) in items.borrow().iter().enumerate() {
72                if i > 0 {
73                    out.push(',');
74                }
75                emit(item, out, depth + 1)?;
76            }
77            out.push(']');
78        }
79        Value::Dict(entries) => {
80            out.push('{');
81            for (i, (key, val)) in entries.borrow().iter().enumerate() {
82                if i > 0 {
83                    out.push(',');
84                }
85                escape_str(key, out, |o, cp| {
86                    let _ = write!(o, "\\u{cp:04x}");
87                });
88                out.push(':');
89                emit(val, out, depth + 1)?;
90            }
91            out.push('}');
92        }
93        other => return Err(unsupported("json", other)),
94    }
95    Ok(())
96}
97
98struct Parser {
99    chars: Vec<char>,
100    pos: usize,
101}
102
103impl Parser {
104    fn error(&self, what: &str) -> DogeError {
105        DogeError::value_error(format!(
106            "json.parse: much invalid. {what} at offset {}",
107            self.pos
108        ))
109    }
110
111    fn peek(&self) -> Option<char> {
112        self.chars.get(self.pos).copied()
113    }
114
115    fn skip_ws(&mut self) {
116        while matches!(self.peek(), Some(' ' | '\t' | '\n' | '\r')) {
117            self.pos += 1;
118        }
119    }
120
121    fn value(&mut self, depth: usize) -> DogeResult {
122        if depth >= MAX_DEPTH {
123            return Err(too_deep("json", "parse"));
124        }
125        match self.peek() {
126            Some('{') => self.object(depth),
127            Some('[') => self.array(depth),
128            Some('"') => Ok(Value::str(self.string()?)),
129            Some('t') => self.keyword("true", Value::Bool(true)),
130            Some('f') => self.keyword("false", Value::Bool(false)),
131            Some('n') => self.keyword("null", Value::None),
132            Some(c) if c == '-' || c.is_ascii_digit() => self.number(),
133            Some(_) => Err(self.error("unexpected character")),
134            None => Err(self.error("unexpected end of input")),
135        }
136    }
137
138    fn keyword(&mut self, word: &str, value: Value) -> DogeResult {
139        for expected in word.chars() {
140            if self.peek() != Some(expected) {
141                return Err(self.error("unexpected character"));
142            }
143            self.pos += 1;
144        }
145        Ok(value)
146    }
147
148    fn object(&mut self, depth: usize) -> DogeResult {
149        self.pos += 1; // consume '{'
150        let mut map = OrderedMap::new();
151        self.skip_ws();
152        if self.peek() == Some('}') {
153            self.pos += 1;
154            return Ok(Value::dict(map));
155        }
156        loop {
157            self.skip_ws();
158            if self.peek() != Some('"') {
159                return Err(self.error("expected a string key"));
160            }
161            let key = self.string()?;
162            self.skip_ws();
163            if self.peek() != Some(':') {
164                return Err(self.error("expected ':'"));
165            }
166            self.pos += 1;
167            self.skip_ws();
168            let val = self.value(depth + 1)?;
169            map.insert(key, val);
170            self.skip_ws();
171            match self.peek() {
172                Some(',') => self.pos += 1,
173                Some('}') => {
174                    self.pos += 1;
175                    return Ok(Value::dict(map));
176                }
177                _ => return Err(self.error("expected ',' or '}'")),
178            }
179        }
180    }
181
182    fn array(&mut self, depth: usize) -> DogeResult {
183        self.pos += 1; // consume '['
184        let mut items = Vec::new();
185        self.skip_ws();
186        if self.peek() == Some(']') {
187            self.pos += 1;
188            return Ok(Value::list(items));
189        }
190        loop {
191            self.skip_ws();
192            items.push(self.value(depth + 1)?);
193            self.skip_ws();
194            match self.peek() {
195                Some(',') => self.pos += 1,
196                Some(']') => {
197                    self.pos += 1;
198                    return Ok(Value::list(items));
199                }
200                _ => return Err(self.error("expected ',' or ']'")),
201            }
202        }
203    }
204
205    fn string(&mut self) -> DogeResult<String> {
206        self.pos += 1; // consume opening '"'
207        let mut s = String::new();
208        loop {
209            match self.peek() {
210                None => return Err(self.error("unterminated string")),
211                Some('"') => {
212                    self.pos += 1;
213                    return Ok(s);
214                }
215                Some('\\') => {
216                    self.pos += 1;
217                    self.escape(&mut s)?;
218                }
219                Some(c) if (c as u32) < 0x20 => {
220                    return Err(self.error("control character in a string"))
221                }
222                Some(c) => {
223                    s.push(c);
224                    self.pos += 1;
225                }
226            }
227        }
228    }
229
230    fn escape(&mut self, s: &mut String) -> DogeResult<()> {
231        match self.peek() {
232            Some('"') => s.push('"'),
233            Some('\\') => s.push('\\'),
234            Some('/') => s.push('/'),
235            Some('b') => s.push('\u{08}'),
236            Some('f') => s.push('\u{0c}'),
237            Some('n') => s.push('\n'),
238            Some('r') => s.push('\r'),
239            Some('t') => s.push('\t'),
240            Some('u') => {
241                self.pos += 1;
242                return self.unicode_escape(s);
243            }
244            _ => return Err(self.error("invalid escape")),
245        }
246        self.pos += 1;
247        Ok(())
248    }
249
250    /// A `\uXXXX` escape (the leading `\u` already consumed). A high surrogate is
251    /// paired with a following `\uXXXX` low surrogate into one code point, so
252    /// astral characters written as surrogate pairs decode correctly.
253    fn unicode_escape(&mut self, s: &mut String) -> DogeResult<()> {
254        let hi = self.hex4()?;
255        let cp = if (0xd800..=0xdbff).contains(&hi) {
256            if self.peek() != Some('\\') {
257                return Err(self.error("lone surrogate in a \\u escape"));
258            }
259            self.pos += 1;
260            if self.peek() != Some('u') {
261                return Err(self.error("lone surrogate in a \\u escape"));
262            }
263            self.pos += 1;
264            let lo = self.hex4()?;
265            if !(0xdc00..=0xdfff).contains(&lo) {
266                return Err(self.error("invalid low surrogate in a \\u escape"));
267            }
268            0x10000 + ((hi - 0xd800) << 10) + (lo - 0xdc00)
269        } else if (0xdc00..=0xdfff).contains(&hi) {
270            return Err(self.error("lone surrogate in a \\u escape"));
271        } else {
272            hi
273        };
274        match char::from_u32(cp) {
275            Some(c) => {
276                s.push(c);
277                Ok(())
278            }
279            None => Err(self.error("invalid code point in a \\u escape")),
280        }
281    }
282
283    fn hex4(&mut self) -> DogeResult<u32> {
284        let mut value = 0u32;
285        for _ in 0..4 {
286            match self.peek().and_then(|c| c.to_digit(16)) {
287                Some(d) => {
288                    value = value * 16 + d;
289                    self.pos += 1;
290                }
291                None => return Err(self.error("expected four hex digits after \\u")),
292            }
293        }
294        Ok(value)
295    }
296
297    /// A JSON number. With neither a fraction nor an exponent it is an Int when it
298    /// fits `i64` (otherwise a Float); with either, it is always a Float.
299    fn number(&mut self) -> DogeResult {
300        let start = self.pos;
301        if self.peek() == Some('-') {
302            self.pos += 1;
303        }
304        self.digits()?;
305        let mut is_float = false;
306        if self.peek() == Some('.') {
307            is_float = true;
308            self.pos += 1;
309            self.digits()?;
310        }
311        if matches!(self.peek(), Some('e' | 'E')) {
312            is_float = true;
313            self.pos += 1;
314            if matches!(self.peek(), Some('+' | '-')) {
315                self.pos += 1;
316            }
317            self.digits()?;
318        }
319        let literal: String = self.chars[start..self.pos].iter().collect();
320        if !is_float {
321            if let Ok(n) = literal.parse::<i64>() {
322                return Ok(Value::Int(n));
323            }
324        }
325        match literal.parse::<f64>() {
326            Ok(x) => Ok(Value::Float(x)),
327            Err(_) => Err(self.error("invalid number")),
328        }
329    }
330
331    fn digits(&mut self) -> DogeResult<()> {
332        if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
333            return Err(self.error("expected a digit"));
334        }
335        while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
336            self.pos += 1;
337        }
338        Ok(())
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::error::ErrorKind;
346
347    fn parse(text: &str) -> DogeResult {
348        json_parse(&Value::str(text))
349    }
350
351    fn emit_str(v: &Value) -> String {
352        match json_emit(v).unwrap() {
353            Value::Str(s) => s.to_string(),
354            other => panic!("expected a Str, got {other:?}"),
355        }
356    }
357
358    #[test]
359    fn scalars_parse_to_the_right_types() {
360        assert!(matches!(parse("null").unwrap(), Value::None));
361        assert!(matches!(parse("true").unwrap(), Value::Bool(true)));
362        assert!(matches!(parse("false").unwrap(), Value::Bool(false)));
363        assert!(matches!(parse("  42 ").unwrap(), Value::Int(42)));
364        assert!(matches!(parse("-7").unwrap(), Value::Int(-7)));
365        assert!(matches!(parse("\"wow\"").unwrap(), Value::Str(s) if &*s == "wow"));
366    }
367
368    #[test]
369    fn a_fraction_or_exponent_is_a_float_but_a_bare_integer_is_an_int() {
370        assert!(matches!(parse("3.5").unwrap(), Value::Float(x) if x == 3.5));
371        assert!(matches!(parse("3.0").unwrap(), Value::Float(x) if x == 3.0));
372        assert!(matches!(parse("1e2").unwrap(), Value::Float(x) if x == 100.0));
373        assert!(matches!(parse("100").unwrap(), Value::Int(100)));
374    }
375
376    #[test]
377    fn an_integer_past_i64_falls_back_to_a_float() {
378        // 2^63 does not fit i64, so it becomes a Float rather than erroring.
379        assert!(matches!(
380            parse("9223372036854775808").unwrap(),
381            Value::Float(_)
382        ));
383    }
384
385    #[test]
386    fn nested_structure_round_trips_through_emit() {
387        let src = "{\"name\":\"kabosu\",\"tags\":[\"doge\",\"shibe\"],\"age\":7,\"good\":true,\"note\":null}";
388        let value = parse(src).unwrap();
389        assert_eq!(emit_str(&value), src);
390    }
391
392    #[test]
393    fn a_surrogate_pair_decodes_to_one_astral_character() {
394        // U+1F415 DOG, written as a UTF-16 surrogate pair.
395        assert!(matches!(parse("\"\\ud83d\\udc15\"").unwrap(), Value::Str(s) if &*s == "🐕"));
396    }
397
398    #[test]
399    fn a_duplicate_key_keeps_the_last_value() {
400        assert_eq!(emit_str(&parse("{\"a\":1,\"a\":2}").unwrap()), "{\"a\":2}");
401    }
402
403    #[test]
404    fn emit_escapes_quotes_and_control_characters() {
405        assert_eq!(
406            emit_str(&Value::str("a\"b\n\t\u{01}")),
407            "\"a\\\"b\\n\\t\\u0001\""
408        );
409    }
410
411    #[test]
412    fn trailing_garbage_is_a_catchable_value_error() {
413        assert_eq!(parse("[1, 2] wow").unwrap_err().kind, ErrorKind::ValueError);
414    }
415
416    #[test]
417    fn deep_nesting_is_a_catchable_error_not_a_stack_overflow() {
418        let deep = "[".repeat(MAX_DEPTH + 5);
419        assert_eq!(parse(&deep).unwrap_err().kind, ErrorKind::ValueError);
420    }
421
422    #[test]
423    fn a_non_finite_float_cannot_be_emitted() {
424        assert_eq!(
425            json_emit(&Value::Float(f64::NAN)).unwrap_err().kind,
426            ErrorKind::ValueError
427        );
428    }
429
430    #[test]
431    fn an_unsupported_type_is_a_catchable_type_error() {
432        let f = Value::function(0, "greet", vec![]);
433        assert_eq!(json_emit(&f).unwrap_err().kind, ErrorKind::TypeError);
434    }
435
436    #[test]
437    fn a_non_str_argument_to_parse_is_a_type_error() {
438        assert_eq!(
439            json_parse(&Value::Int(1)).unwrap_err().kind,
440            ErrorKind::TypeError
441        );
442    }
443}