metrale_grammar/schema/
json_value.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// Order-preserving JSON value — a thin layer over `serde_json`.
4//
5// WHY THIS EXISTS
6// ---------------
7// JSON Schema conversion is order-sensitive: object `properties` must
8// be emitted in declaration order (the C++ uses picojson's
9// `ordered_keys()`). `serde_json::Map` only preserves insertion order
10// when the crate's `preserve_order` feature is enabled, and this
11// crate's `Cargo.toml` may not be modified. So we parse the document
12// once into our own `JsonValue` whose objects keep insertion order.
13//
14// The parser is a straightforward recursive-descent JSON reader. It
15// is not a general-purpose JSON library — it exists only to feed the
16// schema converter with ordered objects. Numbers are kept as both an
17// `f64` and an exact-integer flag so integer-bound checks stay exact.
18
19/// An order-preserving JSON value.
20#[derive(Debug, Clone, PartialEq)]
21pub enum JsonValue {
22    Null,
23    Bool(bool),
24    /// A JSON number. `is_int` is set when the literal had no
25    /// fraction/exponent and fits in `i64`.
26    Number {
27        as_f64: f64,
28        as_i64: Option<i64>,
29    },
30    String(String),
31    Array(Vec<JsonValue>),
32    /// Object with insertion-ordered entries.
33    Object(Vec<(String, JsonValue)>),
34}
35
36impl JsonValue {
37    /// Parse `text` as JSON, preserving object key order.
38    pub fn parse(text: &str) -> Result<JsonValue, String> {
39        let chars: Vec<char> = text.chars().collect();
40        let mut p = Parser { chars, pos: 0 };
41        p.skip_ws();
42        let v = p.parse_value()?;
43        p.skip_ws();
44        if p.pos != p.chars.len() {
45            return Err(format!("trailing characters at position {}", p.pos));
46        }
47        Ok(v)
48    }
49
50    /// Whether this value is a JSON object.
51    pub fn is_object(&self) -> bool {
52        matches!(self, JsonValue::Object(_))
53    }
54
55    /// Borrow as an object's entry slice, if it is one.
56    pub fn as_object(&self) -> Option<&[(String, JsonValue)]> {
57        match self {
58            JsonValue::Object(m) => Some(m),
59            _ => None,
60        }
61    }
62
63    /// Borrow as an array slice, if it is one.
64    pub fn as_array(&self) -> Option<&[JsonValue]> {
65        match self {
66            JsonValue::Array(a) => Some(a),
67            _ => None,
68        }
69    }
70
71    /// Borrow as a string, if it is one.
72    pub fn as_str(&self) -> Option<&str> {
73        match self {
74            JsonValue::String(s) => Some(s),
75            _ => None,
76        }
77    }
78
79    /// Borrow as a bool, if it is one.
80    pub fn as_bool(&self) -> Option<bool> {
81        match self {
82            JsonValue::Bool(b) => Some(*b),
83            _ => None,
84        }
85    }
86
87    /// Look up a key in an object.
88    pub fn get(&self, key: &str) -> Option<&JsonValue> {
89        match self {
90            JsonValue::Object(m) => m.iter().find(|(k, _)| k == key).map(|(_, v)| v),
91            _ => None,
92        }
93    }
94
95    /// Whether an object contains `key`.
96    pub fn contains_key(&self, key: &str) -> bool {
97        self.get(key).is_some()
98    }
99}
100
101struct Parser {
102    chars: Vec<char>,
103    pos: usize,
104}
105
106impl Parser {
107    fn skip_ws(&mut self) {
108        while self.pos < self.chars.len() {
109            match self.chars[self.pos] {
110                ' ' | '\t' | '\n' | '\r' => self.pos += 1,
111                _ => break,
112            }
113        }
114    }
115
116    fn peek(&self) -> Option<char> {
117        self.chars.get(self.pos).copied()
118    }
119
120    fn parse_value(&mut self) -> Result<JsonValue, String> {
121        self.skip_ws();
122        match self.peek() {
123            Some('{') => self.parse_object(),
124            Some('[') => self.parse_array(),
125            Some('"') => Ok(JsonValue::String(self.parse_string()?)),
126            Some('t') | Some('f') => self.parse_bool(),
127            Some('n') => self.parse_null(),
128            Some(c) if c == '-' || c.is_ascii_digit() => self.parse_number(),
129            Some(c) => Err(format!("unexpected character '{c}' at {}", self.pos)),
130            None => Err("unexpected end of input".to_string()),
131        }
132    }
133
134    fn expect(&mut self, c: char) -> Result<(), String> {
135        if self.peek() == Some(c) {
136            self.pos += 1;
137            Ok(())
138        } else {
139            Err(format!("expected '{c}' at position {}", self.pos))
140        }
141    }
142
143    fn parse_object(&mut self) -> Result<JsonValue, String> {
144        self.expect('{')?;
145        let mut entries: Vec<(String, JsonValue)> = Vec::new();
146        self.skip_ws();
147        if self.peek() == Some('}') {
148            self.pos += 1;
149            return Ok(JsonValue::Object(entries));
150        }
151        loop {
152            self.skip_ws();
153            let key = self.parse_string()?;
154            self.skip_ws();
155            self.expect(':')?;
156            let value = self.parse_value()?;
157            // Last-key-wins, matching JSON object semantics.
158            if let Some(slot) = entries.iter_mut().find(|(k, _)| *k == key) {
159                slot.1 = value;
160            } else {
161                entries.push((key, value));
162            }
163            self.skip_ws();
164            match self.peek() {
165                Some(',') => self.pos += 1,
166                Some('}') => {
167                    self.pos += 1;
168                    break;
169                }
170                _ => return Err(format!("expected ',' or '}}' at {}", self.pos)),
171            }
172        }
173        Ok(JsonValue::Object(entries))
174    }
175
176    fn parse_array(&mut self) -> Result<JsonValue, String> {
177        self.expect('[')?;
178        let mut items = Vec::new();
179        self.skip_ws();
180        if self.peek() == Some(']') {
181            self.pos += 1;
182            return Ok(JsonValue::Array(items));
183        }
184        loop {
185            items.push(self.parse_value()?);
186            self.skip_ws();
187            match self.peek() {
188                Some(',') => self.pos += 1,
189                Some(']') => {
190                    self.pos += 1;
191                    break;
192                }
193                _ => return Err(format!("expected ',' or ']' at {}", self.pos)),
194            }
195        }
196        Ok(JsonValue::Array(items))
197    }
198
199    fn parse_string(&mut self) -> Result<String, String> {
200        self.expect('"')?;
201        let mut s = String::new();
202        loop {
203            match self.peek() {
204                None => return Err("unterminated string".to_string()),
205                Some('"') => {
206                    self.pos += 1;
207                    break;
208                }
209                Some('\\') => {
210                    self.pos += 1;
211                    let esc = self.peek().ok_or("dangling escape")?;
212                    self.pos += 1;
213                    match esc {
214                        '"' => s.push('"'),
215                        '\\' => s.push('\\'),
216                        '/' => s.push('/'),
217                        'b' => s.push('\u{0008}'),
218                        'f' => s.push('\u{000C}'),
219                        'n' => s.push('\n'),
220                        'r' => s.push('\r'),
221                        't' => s.push('\t'),
222                        'u' => {
223                            let cp = self.parse_hex4()?;
224                            if (0xD800..=0xDBFF).contains(&cp) {
225                                // High surrogate: expect a low surrogate.
226                                if self.peek() == Some('\\') {
227                                    self.pos += 1;
228                                    self.expect('u')?;
229                                    let lo = self.parse_hex4()?;
230                                    let c = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
231                                    s.push(char::from_u32(c).ok_or("invalid surrogate pair")?);
232                                } else {
233                                    return Err("lone high surrogate".to_string());
234                                }
235                            } else {
236                                s.push(char::from_u32(cp).ok_or("invalid \\u escape")?);
237                            }
238                        }
239                        other => return Err(format!("bad escape '\\{other}'")),
240                    }
241                }
242                Some(c) => {
243                    self.pos += 1;
244                    s.push(c);
245                }
246            }
247        }
248        Ok(s)
249    }
250
251    fn parse_hex4(&mut self) -> Result<u32, String> {
252        let mut v = 0u32;
253        for _ in 0..4 {
254            let c = self.peek().ok_or("short \\u escape")?;
255            let d = c.to_digit(16).ok_or("bad hex digit")?;
256            v = v * 16 + d;
257            self.pos += 1;
258        }
259        Ok(v)
260    }
261
262    fn parse_bool(&mut self) -> Result<JsonValue, String> {
263        if self.matches_literal("true") {
264            Ok(JsonValue::Bool(true))
265        } else if self.matches_literal("false") {
266            Ok(JsonValue::Bool(false))
267        } else {
268            Err(format!("invalid literal at {}", self.pos))
269        }
270    }
271
272    fn parse_null(&mut self) -> Result<JsonValue, String> {
273        if self.matches_literal("null") {
274            Ok(JsonValue::Null)
275        } else {
276            Err(format!("invalid literal at {}", self.pos))
277        }
278    }
279
280    fn matches_literal(&mut self, lit: &str) -> bool {
281        let lit_chars: Vec<char> = lit.chars().collect();
282        if self.pos + lit_chars.len() > self.chars.len() {
283            return false;
284        }
285        if self.chars[self.pos..self.pos + lit_chars.len()] == lit_chars[..] {
286            self.pos += lit_chars.len();
287            true
288        } else {
289            false
290        }
291    }
292
293    fn parse_number(&mut self) -> Result<JsonValue, String> {
294        let start = self.pos;
295        let mut is_int = true;
296        if self.peek() == Some('-') {
297            self.pos += 1;
298        }
299        while let Some(c) = self.peek() {
300            match c {
301                '0'..='9' => self.pos += 1,
302                '.' | 'e' | 'E' | '+' | '-' => {
303                    is_int = false;
304                    self.pos += 1;
305                }
306                _ => break,
307            }
308        }
309        let text: String = self.chars[start..self.pos].iter().collect();
310        let as_f64: f64 = text
311            .parse()
312            .map_err(|_| format!("invalid number '{text}'"))?;
313        let as_i64 = if is_int {
314            text.parse::<i64>().ok()
315        } else {
316            None
317        };
318        Ok(JsonValue::Number { as_f64, as_i64 })
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn parses_ordered_object() {
328        let v = JsonValue::parse(r#"{"b":1,"a":2,"c":3}"#).unwrap();
329        let keys: Vec<&str> = v
330            .as_object()
331            .unwrap()
332            .iter()
333            .map(|(k, _)| k.as_str())
334            .collect();
335        assert_eq!(keys, ["b", "a", "c"]);
336    }
337
338    #[test]
339    fn parses_scalars_and_nesting() {
340        let v = JsonValue::parse(r#"{"x":[true,null,-3.5,"hi"]}"#).unwrap();
341        let arr = v.get("x").unwrap().as_array().unwrap();
342        assert_eq!(arr[0], JsonValue::Bool(true));
343        assert_eq!(arr[1], JsonValue::Null);
344        assert_eq!(arr[3].as_str(), Some("hi"));
345    }
346
347    #[test]
348    fn integer_flag_is_exact() {
349        let v = JsonValue::parse("42").unwrap();
350        match v {
351            JsonValue::Number { as_i64, .. } => assert_eq!(as_i64, Some(42)),
352            _ => panic!("expected number"),
353        }
354        let f = JsonValue::parse("42.0").unwrap();
355        match f {
356            JsonValue::Number { as_i64, .. } => assert_eq!(as_i64, None),
357            _ => panic!("expected number"),
358        }
359    }
360
361    #[test]
362    fn rejects_trailing_junk() {
363        assert!(JsonValue::parse("{} extra").is_err());
364        assert!(JsonValue::parse("{").is_err());
365    }
366
367    #[test]
368    fn parses_escapes() {
369        let v = JsonValue::parse(r#""a\nbA""#).unwrap();
370        assert_eq!(v.as_str(), Some("a\nbA"));
371    }
372}