Skip to main content

yog_cli/content/
json.rs

1//! A small JSON reader that remembers the line of every value, so `.yblock`
2//! and `.yitem` errors point at `file:line` (serde_json only knows where a
3//! *syntax* error is).
4
5/// A JSON value and the 1-based line it starts on.
6#[derive(Debug, Clone, PartialEq)]
7pub struct J {
8    pub line: usize,
9    pub v: V,
10}
11
12#[derive(Debug, Clone, PartialEq)]
13pub enum V {
14    Null,
15    Bool(bool),
16    Num(f64),
17    Str(String),
18    Arr(Vec<J>),
19    /// Members in file order.
20    Obj(Vec<(String, J)>),
21}
22
23impl J {
24    pub fn get(&self, key: &str) -> Option<&J> {
25        match &self.v {
26            V::Obj(members) => members.iter().find(|(k, _)| k == key).map(|(_, v)| v),
27            _ => None,
28        }
29    }
30
31    pub fn members(&self) -> &[(String, J)] {
32        match &self.v {
33            V::Obj(m) => m,
34            _ => &[],
35        }
36    }
37
38    pub fn items(&self) -> &[J] {
39        match &self.v {
40            V::Arr(a) => a,
41            _ => &[],
42        }
43    }
44
45    pub fn str(&self) -> Option<&str> {
46        match &self.v {
47            V::Str(s) => Some(s),
48            _ => None,
49        }
50    }
51
52    pub fn num(&self) -> Option<f64> {
53        match self.v {
54            V::Num(n) => Some(n),
55            _ => None,
56        }
57    }
58
59    pub fn bool(&self) -> Option<bool> {
60        match self.v {
61            V::Bool(b) => Some(b),
62            _ => None,
63        }
64    }
65
66    pub fn is_obj(&self) -> bool {
67        matches!(self.v, V::Obj(_))
68    }
69
70    pub fn is_arr(&self) -> bool {
71        matches!(self.v, V::Arr(_))
72    }
73
74    /// What kind of value this is, for messages ("a string", "an object").
75    pub fn kind(&self) -> &'static str {
76        match self.v {
77            V::Null => "null",
78            V::Bool(_) => "a boolean",
79            V::Num(_) => "a number",
80            V::Str(_) => "a string",
81            V::Arr(_) => "an array",
82            V::Obj(_) => "an object",
83        }
84    }
85
86    /// The plain serde_json form (lines dropped).
87    #[cfg(test)]
88    pub fn to_value(&self) -> serde_json::Value {
89        use serde_json::Value;
90        match &self.v {
91            V::Null => Value::Null,
92            V::Bool(b) => Value::Bool(*b),
93            V::Num(n) => num_value(*n),
94            V::Str(s) => Value::String(s.clone()),
95            V::Arr(a) => Value::Array(a.iter().map(J::to_value).collect()),
96            V::Obj(m) => Value::Object(m.iter().map(|(k, v)| (k.clone(), v.to_value())).collect()),
97        }
98    }
99}
100
101/// A number as JSON: integral values without a fraction (`16`, not `16.0`).
102pub fn num_value(n: f64) -> serde_json::Value {
103    if n.fract() == 0.0 && n.abs() < 1e15 {
104        serde_json::Value::from(n as i64)
105    } else {
106        serde_json::Number::from_f64(n)
107            .map(serde_json::Value::Number)
108            .unwrap_or(serde_json::Value::Null)
109    }
110}
111
112/// Builds values for [`to_pretty`]: `J::obj([("a", J::number(1.0))])`.
113impl J {
114    pub fn of(v: V) -> J {
115        J { line: 0, v }
116    }
117
118    pub fn obj<K: Into<String>>(members: impl IntoIterator<Item = (K, J)>) -> J {
119        J::of(V::Obj(
120            members.into_iter().map(|(k, v)| (k.into(), v)).collect(),
121        ))
122    }
123
124    pub fn number(n: f64) -> J {
125        J::of(V::Num(n))
126    }
127
128    pub fn string(s: impl Into<String>) -> J {
129        J::of(V::Str(s.into()))
130    }
131
132    pub fn nums(ns: &[f64]) -> J {
133        J::of(V::Arr(ns.iter().map(|n| J::number(*n)).collect()))
134    }
135
136    /// From serde_json (keys in its order).
137    pub fn from_value(v: &serde_json::Value) -> J {
138        use serde_json::Value;
139        J::of(match v {
140            Value::Null => V::Null,
141            Value::Bool(b) => V::Bool(*b),
142            Value::Number(n) => V::Num(n.as_f64().unwrap_or(0.0)),
143            Value::String(s) => V::Str(s.clone()),
144            Value::Array(a) => V::Arr(a.iter().map(J::from_value).collect()),
145            Value::Object(m) => V::Obj(
146                m.iter()
147                    .map(|(k, v)| (k.clone(), J::from_value(v)))
148                    .collect(),
149            ),
150        })
151    }
152
153    /// Adds or replaces a member of an object.
154    pub fn set(&mut self, key: &str, value: J) {
155        if let V::Obj(m) = &mut self.v {
156            match m.iter_mut().find(|(k, _)| k == key) {
157                Some((_, v)) => *v = value,
158                None => m.push((key.to_string(), value)),
159            }
160        }
161    }
162}
163
164/// Pretty JSON, keys in order, two-space indent; arrays of numbers and
165/// small objects of scalars stay on one line (`"uv": [0, 0, 16, 16]`).
166pub fn to_pretty(j: &J) -> String {
167    fn scalar(j: &J) -> Option<String> {
168        Some(match &j.v {
169            V::Null => "null".into(),
170            V::Bool(b) => b.to_string(),
171            V::Num(n) => num_value(*n).to_string(),
172            V::Str(s) => serde_json::Value::String(s.clone()).to_string(),
173            _ => return None,
174        })
175    }
176    fn inline(j: &J) -> Option<String> {
177        if let Some(s) = scalar(j) {
178            return Some(s);
179        }
180        let s = match &j.v {
181            V::Arr(a)
182                if a.iter()
183                    .all(|i| matches!(i.v, V::Num(_) | V::Bool(_) | V::Null)) =>
184            {
185                format!(
186                    "[{}]",
187                    a.iter().filter_map(scalar).collect::<Vec<_>>().join(", ")
188                )
189            }
190            V::Obj(m)
191                if !m.is_empty()
192                    && m.iter()
193                        .all(|(_, v)| inline(v).is_some() && !matches!(v.v, V::Obj(_))) =>
194            {
195                let parts: Vec<String> = m
196                    .iter()
197                    .filter_map(|(k, v)| {
198                        Some(format!(
199                            "{}: {}",
200                            serde_json::Value::String(k.clone()),
201                            inline(v)?
202                        ))
203                    })
204                    .collect();
205                format!("{{ {} }}", parts.join(", "))
206            }
207            V::Obj(m) if m.is_empty() => "{}".into(),
208            V::Arr(a) if a.is_empty() => "[]".into(),
209            _ => return None,
210        };
211        (s.len() <= 72).then_some(s)
212    }
213    fn write(j: &J, indent: usize, out: &mut String) {
214        if let Some(s) = inline(j) {
215            out.push_str(&s);
216            return;
217        }
218        let pad = "  ".repeat(indent + 1);
219        match &j.v {
220            V::Arr(a) => {
221                out.push_str("[\n");
222                for (i, item) in a.iter().enumerate() {
223                    out.push_str(&pad);
224                    write(item, indent + 1, out);
225                    out.push_str(if i + 1 < a.len() { ",\n" } else { "\n" });
226                }
227                out.push_str(&"  ".repeat(indent));
228                out.push(']');
229            }
230            V::Obj(m) => {
231                out.push_str("{\n");
232                for (i, (k, v)) in m.iter().enumerate() {
233                    out.push_str(&pad);
234                    out.push_str(&serde_json::Value::String(k.clone()).to_string());
235                    out.push_str(": ");
236                    write(v, indent + 1, out);
237                    out.push_str(if i + 1 < m.len() { ",\n" } else { "\n" });
238                }
239                out.push_str(&"  ".repeat(indent));
240                out.push('}');
241            }
242            _ => {}
243        }
244    }
245    let mut out = String::new();
246    write(j, 0, &mut out);
247    out.push('\n');
248    out
249}
250
251/// Parses `text`. Errors are `line N: message`. Duplicate keys are an error.
252pub fn parse(text: &str) -> Result<J, String> {
253    let mut p = P {
254        s: text.as_bytes(),
255        i: 0,
256        line: 1,
257    };
258    if p.s.starts_with(b"\xEF\xBB\xBF") {
259        p.i = 3;
260    }
261    p.ws();
262    let v = p.value(0)?;
263    p.ws();
264    if p.i < p.s.len() {
265        return Err(p.err("unexpected text after the JSON value"));
266    }
267    Ok(v)
268}
269
270struct P<'a> {
271    s: &'a [u8],
272    i: usize,
273    line: usize,
274}
275
276impl P<'_> {
277    fn err(&self, msg: &str) -> String {
278        format!("line {}: {msg}", self.line)
279    }
280
281    fn peek(&self) -> Option<u8> {
282        self.s.get(self.i).copied()
283    }
284
285    fn ws(&mut self) {
286        while let Some(c) = self.peek() {
287            match c {
288                b'\n' => {
289                    self.line += 1;
290                    self.i += 1;
291                }
292                b' ' | b'\t' | b'\r' => self.i += 1,
293                // `//` comments are tolerated (hand-edited files).
294                b'/' if self.s.get(self.i + 1) == Some(&b'/') => {
295                    while self.peek().is_some_and(|c| c != b'\n') {
296                        self.i += 1;
297                    }
298                }
299                _ => break,
300            }
301        }
302    }
303
304    fn eat(&mut self, c: u8) -> bool {
305        if self.peek() == Some(c) {
306            self.i += 1;
307            true
308        } else {
309            false
310        }
311    }
312
313    fn value(&mut self, depth: usize) -> Result<J, String> {
314        if depth > 64 {
315            return Err(self.err("nested too deeply"));
316        }
317        let line = self.line;
318        let v = match self.peek() {
319            Some(b'{') => {
320                self.i += 1;
321                let mut members: Vec<(String, J)> = Vec::new();
322                self.ws();
323                if !self.eat(b'}') {
324                    loop {
325                        self.ws();
326                        let key_line = self.line;
327                        if self.peek() != Some(b'"') {
328                            return Err(self.err("expected a \"key\""));
329                        }
330                        let key = self.string()?;
331                        if members.iter().any(|(k, _)| *k == key) {
332                            return Err(format!("line {key_line}: duplicate key {key:?}"));
333                        }
334                        self.ws();
335                        if !self.eat(b':') {
336                            return Err(self.err("expected `:` after the key"));
337                        }
338                        self.ws();
339                        let mut v = self.value(depth + 1)?;
340                        // A member's line is its key's (where an editor shows it).
341                        v.line = key_line;
342                        members.push((key, v));
343                        self.ws();
344                        if self.eat(b',') {
345                            self.ws();
346                            // Trailing comma tolerated.
347                            if self.eat(b'}') {
348                                break;
349                            }
350                            continue;
351                        }
352                        if self.eat(b'}') {
353                            break;
354                        }
355                        return Err(self.err("expected `,` or `}`"));
356                    }
357                }
358                V::Obj(members)
359            }
360            Some(b'[') => {
361                self.i += 1;
362                let mut items = Vec::new();
363                self.ws();
364                if !self.eat(b']') {
365                    loop {
366                        self.ws();
367                        items.push(self.value(depth + 1)?);
368                        self.ws();
369                        if self.eat(b',') {
370                            self.ws();
371                            if self.eat(b']') {
372                                break;
373                            }
374                            continue;
375                        }
376                        if self.eat(b']') {
377                            break;
378                        }
379                        return Err(self.err("expected `,` or `]`"));
380                    }
381                }
382                V::Arr(items)
383            }
384            Some(b'"') => V::Str(self.string()?),
385            Some(b't') if self.s[self.i..].starts_with(b"true") => {
386                self.i += 4;
387                V::Bool(true)
388            }
389            Some(b'f') if self.s[self.i..].starts_with(b"false") => {
390                self.i += 5;
391                V::Bool(false)
392            }
393            Some(b'n') if self.s[self.i..].starts_with(b"null") => {
394                self.i += 4;
395                V::Null
396            }
397            Some(c) if c == b'-' || c.is_ascii_digit() => {
398                let start = self.i;
399                self.i += 1;
400                while self.peek().is_some_and(|c| {
401                    c.is_ascii_digit() || matches!(c, b'.' | b'e' | b'E' | b'+' | b'-')
402                }) {
403                    self.i += 1;
404                }
405                let lit = std::str::from_utf8(&self.s[start..self.i]).unwrap_or("");
406                V::Num(
407                    lit.parse()
408                        .map_err(|_| self.err(&format!("`{lit}` isn't a number")))?,
409                )
410            }
411            None => return Err(self.err("unexpected end of file")),
412            Some(_) => return Err(self.err("expected a JSON value")),
413        };
414        Ok(J { line, v })
415    }
416
417    fn string(&mut self) -> Result<String, String> {
418        if !self.eat(b'"') {
419            return Err(self.err("expected `\"`"));
420        }
421        let mut out: Vec<u8> = Vec::new();
422        loop {
423            let Some(c) = self.peek() else {
424                return Err(self.err("unterminated string"));
425            };
426            self.i += 1;
427            match c {
428                b'"' => break,
429                b'\n' => return Err(self.err("line break inside a string (write \\n)")),
430                b'\\' => {
431                    let Some(e) = self.peek() else {
432                        return Err(self.err("unterminated string"));
433                    };
434                    self.i += 1;
435                    match e {
436                        b'"' => out.push(b'"'),
437                        b'\\' => out.push(b'\\'),
438                        b'/' => out.push(b'/'),
439                        b'b' => out.push(8),
440                        b'f' => out.push(12),
441                        b'n' => out.push(b'\n'),
442                        b'r' => out.push(b'\r'),
443                        b't' => out.push(b'\t'),
444                        b'u' => {
445                            let mut code = self.hex4()?;
446                            if (0xD800..0xDC00).contains(&code)
447                                && self.s[self.i..].starts_with(b"\\u")
448                            {
449                                self.i += 2;
450                                let low = self.hex4()?;
451                                code = 0x10000
452                                    + ((code - 0xD800) << 10)
453                                    + (low.wrapping_sub(0xDC00) & 0x3FF);
454                            }
455                            let ch = char::from_u32(code).unwrap_or('\u{FFFD}');
456                            let mut buf = [0u8; 4];
457                            out.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
458                        }
459                        _ => return Err(self.err("unknown escape in a string")),
460                    }
461                }
462                c => out.push(c),
463            }
464        }
465        String::from_utf8(out).map_err(|_| self.err("invalid UTF-8 in a string"))
466    }
467
468    fn hex4(&mut self) -> Result<u32, String> {
469        let hex = self
470            .s
471            .get(self.i..self.i + 4)
472            .and_then(|h| std::str::from_utf8(h).ok())
473            .and_then(|h| u32::from_str_radix(h, 16).ok())
474            .ok_or_else(|| self.err("bad \\u escape"))?;
475        self.i += 4;
476        Ok(hex)
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    #[test]
485    fn records_lines_of_members() {
486        let j = parse("{\n  \"a\": 1,\n  \"b\": [\n    true,\n    \"x\"\n  ]\n}").unwrap();
487        assert_eq!(j.line, 1);
488        assert_eq!(j.get("a").unwrap().line, 2);
489        let b = j.get("b").unwrap();
490        assert_eq!(b.line, 3);
491        assert_eq!(b.items()[1].line, 5);
492        assert_eq!(b.items()[1].str(), Some("x"));
493    }
494
495    #[test]
496    fn rejects_duplicates_and_reports_line() {
497        let e = parse("{\n\"a\": 1,\n\"a\": 2}").unwrap_err();
498        assert!(e.starts_with("line 3:"), "{e}");
499    }
500
501    #[test]
502    fn tolerates_comments_and_trailing_commas() {
503        let j = parse("{ // note\n \"a\": [1, 2,],\n}").unwrap();
504        assert_eq!(j.get("a").unwrap().items().len(), 2);
505    }
506
507    #[test]
508    fn pretty_prints_in_order() {
509        let j = J::obj([
510            ("b", J::nums(&[0.0, 16.0])),
511            ("a", J::obj([("x", J::string("y"))])),
512        ]);
513        assert_eq!(
514            to_pretty(&j),
515            "{\n  \"b\": [0, 16],\n  \"a\": { \"x\": \"y\" }\n}\n"
516        );
517        assert_eq!(parse(&to_pretty(&j)).unwrap().to_value(), j.to_value());
518    }
519
520    #[test]
521    fn integral_numbers_stay_integral() {
522        assert_eq!(num_value(16.0).to_string(), "16");
523        assert_eq!(num_value(22.5).to_string(), "22.5");
524    }
525}