aede_core/
json.rs

1//! Minimal JSON implementation (reading + writing), with no external
2//! dependency.
3//!
4//! Aède persists its catalog in a JSON file whose structure mirrors the target
5//! relational schema (`schema.sql`) exactly: one key per "table", each table
6//! being an array of rows. Storage remains JSON; a future database migration
7//! can preserve these concepts without changing the domain model.
8
9use std::collections::BTreeMap;
10mod output;
11use output::Output;
12
13/// A JSON value held in memory, as the catalog manipulates it between the file and the model.
14#[derive(Debug, Clone, PartialEq)]
15pub enum Json {
16    /// The `null` literal, which the catalog uses for an absent or unknown field.
17    Null,
18    /// The `true` and `false` literals.
19    Bool(bool),
20    /// A number: JSON knows only one numeric type, kept here as a double.
21    Num(f64),
22    /// A text string, already unescaped: the characters it denotes, not its written form.
23    Str(String),
24    /// An ordered sequence of values, the JSON array; a table of the schema is one of these.
25    Arr(Vec<Json>),
26    /// A set of key/value pairs, kept sorted by key so that two writes give the same bytes.
27    Obj(BTreeMap<String, Json>),
28}
29
30impl Json {
31    /// Opens an empty object, the starting point for building a row field by field.
32    pub fn obj() -> Json {
33        Json::Obj(BTreeMap::new())
34    }
35
36    /// Adds or replaces a field. The call is silently ignored when the value is not an object.
37    pub fn set(&mut self, key: &str, value: Json) {
38        if let Json::Obj(map) = self {
39            map.insert(key.to_string(), value);
40        }
41    }
42
43    /// Looks up a field by name; absent key and non-object alike give `None`, without failing.
44    pub fn get(&self, key: &str) -> Option<&Json> {
45        match self {
46            Json::Obj(map) => map.get(key),
47            _ => None,
48        }
49    }
50
51    /// Borrows the text of a string value, with no copy; anything else gives `None`.
52    pub fn as_str(&self) -> Option<&str> {
53        match self {
54            Json::Str(s) => Some(s),
55            _ => None,
56        }
57    }
58
59    /// Same reading as [`Json::as_str`], copied out for a caller that needs to keep the text.
60    pub fn as_string(&self) -> Option<String> {
61        self.as_str().map(|s| s.to_string())
62    }
63
64    /// Reads a number as stored, with no conversion or rounding.
65    pub fn as_f64(&self) -> Option<f64> {
66        match self {
67            Json::Num(n) => Some(*n),
68            _ => None,
69        }
70    }
71
72    /// Reads an integral, finite count in the `u64` range. Fractional and
73    /// out-of-range values are refused instead of being rounded or clamped.
74    pub fn as_u64(&self) -> Option<u64> {
75        self.as_f64().and_then(|n| {
76            // u64::MAX rounds up to 2^64 as f64, so the upper bound is strict.
77            if n.is_finite() && n >= 0.0 && n < u64::MAX as f64 && n.fract() == 0.0 {
78                Some(n as u64)
79            } else {
80                None
81            }
82        })
83    }
84
85    /// Same reading as [`Json::as_u64`], narrowed for the small counters of the schema such as a
86    /// year or a track number.
87    pub fn as_u32(&self) -> Option<u32> {
88        self.as_u64().and_then(|n| u32::try_from(n).ok())
89    }
90
91    /// Reads a boolean value; a `0` or a `"true"` string is not accepted as one.
92    pub fn as_bool(&self) -> Option<bool> {
93        match self {
94            Json::Bool(b) => Some(*b),
95            _ => None,
96        }
97    }
98
99    /// Borrows the elements of an array, the usual way to walk over the rows of a table.
100    pub fn as_arr(&self) -> Option<&[Json]> {
101        match self {
102            Json::Arr(v) => Some(v),
103            _ => None,
104        }
105    }
106
107    /// Direct access to a typed field, tolerating a missing key.
108    pub fn field_str(&self, key: &str) -> Option<String> {
109        self.get(key).and_then(|v| v.as_string())
110    }
111
112    /// Reads a field as a count, absent or unusable giving `None`.
113    pub fn field_u64(&self, key: &str) -> Option<u64> {
114        self.get(key).and_then(|v| v.as_u64())
115    }
116
117    /// Reads a field as a small count, absent or unusable giving `None`.
118    pub fn field_u32(&self, key: &str) -> Option<u32> {
119        self.get(key).and_then(|v| v.as_u32())
120    }
121
122    /// Reads a numeric field that may be fractional.
123    pub fn field_f64(&self, key: &str) -> Option<f64> {
124        self.get(key).and_then(|v| v.as_f64())
125    }
126
127    /// Reads a boolean field, `None` when it is absent or of another type.
128    ///
129    /// Distinct from [`Json::field_bool`], which answers `false` for both: an
130    /// imported measurement that was never taken is not a measurement that
131    /// came out negative.
132    pub fn field_optional_bool(&self, key: &str) -> Option<bool> {
133        self.get(key).and_then(|v| v.as_bool())
134    }
135
136    /// Reads a flag, where an absent field means `false`: an older catalog that predates the flag
137    /// is read without special handling.
138    pub fn field_bool(&self, key: &str) -> bool {
139        self.get(key).and_then(|v| v.as_bool()).unwrap_or(false)
140    }
141}
142
143impl From<&str> for Json {
144    fn from(s: &str) -> Json {
145        Json::Str(s.to_string())
146    }
147}
148impl From<String> for Json {
149    fn from(s: String) -> Json {
150        Json::Str(s)
151    }
152}
153impl From<u64> for Json {
154    fn from(n: u64) -> Json {
155        Json::Num(n as f64)
156    }
157}
158impl From<u32> for Json {
159    fn from(n: u32) -> Json {
160        Json::Num(n as f64)
161    }
162}
163impl From<usize> for Json {
164    fn from(n: usize) -> Json {
165        Json::Num(n as f64)
166    }
167}
168impl From<i64> for Json {
169    fn from(n: i64) -> Json {
170        Json::Num(n as f64)
171    }
172}
173impl From<f64> for Json {
174    fn from(n: f64) -> Json {
175        Json::Num(n)
176    }
177}
178impl From<bool> for Json {
179    fn from(b: bool) -> Json {
180        Json::Bool(b)
181    }
182}
183impl<T: Into<Json>> From<Option<T>> for Json {
184    fn from(v: Option<T>) -> Json {
185        match v {
186            Some(x) => x.into(),
187            None => Json::Null,
188        }
189    }
190}
191impl<T: Into<Json>> From<Vec<T>> for Json {
192    fn from(v: Vec<T>) -> Json {
193        Json::Arr(v.into_iter().map(Into::into).collect())
194    }
195}
196
197// --------------------------------------------------------------------------
198// Writing
199// --------------------------------------------------------------------------
200
201impl Json {
202    /// Serializes to compact JSON.
203    pub fn to_string_compact(&self) -> String {
204        let mut out = String::new();
205        self.write(&mut out, None, 0);
206        out
207    }
208
209    /// Serializes to indented JSON, easier to inspect by hand.
210    pub fn to_string_pretty(&self) -> String {
211        let mut out = String::new();
212        self.write(&mut out, Some(2), 0);
213        out
214    }
215
216    /// Serialize the same indented JSON, refusing output beyond a byte budget.
217    /// This bounds the serialized bytes, including escaping and whitespace.
218    pub(crate) fn to_string_pretty_bounded(
219        &self,
220        maximum: usize,
221    ) -> Result<String, std::fmt::Error> {
222        let mut out = output::Limited::new(maximum);
223        self.write(&mut out, Some(2), 0);
224        out.finish()
225    }
226
227    pub(crate) fn check_pretty_size(&self, maximum: usize) -> std::fmt::Result {
228        let mut out = output::Counter::new(maximum);
229        self.write(&mut out, Some(2), 0);
230        out.finish()
231    }
232
233    fn write(&self, out: &mut impl Output, indent: Option<usize>, depth: usize) {
234        if out.exhausted() {
235            return;
236        }
237        match self {
238            Json::Null => out.push_str("null"),
239            Json::Bool(true) => out.push_str("true"),
240            Json::Bool(false) => out.push_str("false"),
241            Json::Num(n) => {
242                if n.is_finite() {
243                    if n.fract() == 0.0 && n.abs() < 9.0e15 {
244                        let _ = write!(out, "{}", *n as i64);
245                    } else {
246                        let _ = write!(out, "{}", n);
247                    }
248                } else {
249                    out.push_str("null");
250                }
251            }
252            Json::Str(s) => write_escaped(out, s),
253            Json::Arr(items) => {
254                if items.is_empty() {
255                    out.push_str("[]");
256                    return;
257                }
258                out.push('[');
259                for (i, item) in items.iter().enumerate() {
260                    if out.exhausted() {
261                        return;
262                    }
263                    if i > 0 {
264                        out.push(',');
265                    }
266                    newline_indent(out, indent, depth + 1);
267                    item.write(out, indent, depth + 1);
268                }
269                newline_indent(out, indent, depth);
270                out.push(']');
271            }
272            Json::Obj(map) => {
273                if map.is_empty() {
274                    out.push_str("{}");
275                    return;
276                }
277                out.push('{');
278                for (i, (k, v)) in map.iter().enumerate() {
279                    if out.exhausted() {
280                        return;
281                    }
282                    if i > 0 {
283                        out.push(',');
284                    }
285                    newline_indent(out, indent, depth + 1);
286                    write_escaped(out, k);
287                    out.push(':');
288                    if indent.is_some() {
289                        out.push(' ');
290                    }
291                    v.write(out, indent, depth + 1);
292                }
293                newline_indent(out, indent, depth);
294                out.push('}');
295            }
296        }
297    }
298}
299
300fn newline_indent(out: &mut impl Output, indent: Option<usize>, depth: usize) {
301    if let Some(width) = indent {
302        out.push('\n');
303        for _ in 0..(width * depth) {
304            out.push(' ');
305        }
306    }
307}
308
309fn write_escaped(out: &mut impl Output, s: &str) {
310    out.push('"');
311    for ch in s.chars() {
312        if out.exhausted() {
313            return;
314        }
315        match ch {
316            '"' => out.push_str("\\\""),
317            '\\' => out.push_str("\\\\"),
318            '\n' => out.push_str("\\n"),
319            '\r' => out.push_str("\\r"),
320            '\t' => out.push_str("\\t"),
321            '\u{08}' => out.push_str("\\b"),
322            '\u{0c}' => out.push_str("\\f"),
323            c if (c as u32) < 0x20 => {
324                let _ = write!(out, "\\u{:04x}", c as u32);
325            }
326            c => out.push(c),
327        }
328    }
329    out.push('"');
330}
331
332// --------------------------------------------------------------------------
333// Reading
334// --------------------------------------------------------------------------
335
336/// Reason a document was refused, with enough detail to point at the guilty spot in the file.
337#[derive(Debug)]
338pub struct ParseError {
339    /// What was expected at that point, phrased for someone opening the catalog in an editor.
340    pub message: String,
341    /// Position of the fault, counted in bytes from the start of the input.
342    pub offset: usize,
343}
344
345impl std::fmt::Display for ParseError {
346    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347        write!(f, "invalid JSON at byte {}: {}", self.offset, self.message)
348    }
349}
350
351impl std::error::Error for ParseError {}
352
353/// Reads a whole document and returns its root value.
354///
355/// The entire input must be consumed: anything after the root value is an error, so a truncated
356/// or doubly written catalog is caught rather than half loaded.
357/// Malformed Unicode, non-finite numbers and more than 128 nested containers
358/// are rejected. Parse failures include a byte offset.
359///
360/// ```
361/// let value = aede_core::json::parse(r#"{"count":3}"#)?;
362/// assert_eq!(value.field_u32("count"), Some(3));
363/// assert!(aede_core::json::parse("01").is_err());
364/// # Ok::<(), aede_core::json::ParseError>(())
365/// ```
366pub fn parse(input: &str) -> Result<Json, ParseError> {
367    let bytes = input.as_bytes();
368    let mut p = Parser {
369        bytes,
370        pos: 0,
371        depth: 0,
372    };
373    p.skip_ws();
374    let value = p.value()?;
375    p.skip_ws();
376    if p.pos != bytes.len() {
377        return Err(p.err("trailing data after the root value"));
378    }
379    Ok(value)
380}
381
382struct Parser<'a> {
383    bytes: &'a [u8],
384    pos: usize,
385    depth: usize,
386}
387
388impl<'a> Parser<'a> {
389    fn err(&self, msg: &str) -> ParseError {
390        ParseError {
391            message: msg.to_string(),
392            offset: self.pos,
393        }
394    }
395
396    fn peek(&self) -> Option<u8> {
397        self.bytes.get(self.pos).copied()
398    }
399
400    fn skip_ws(&mut self) {
401        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
402            self.pos += 1;
403        }
404    }
405
406    fn expect(&mut self, byte: u8) -> Result<(), ParseError> {
407        if self.peek() == Some(byte) {
408            self.pos += 1;
409            Ok(())
410        } else {
411            Err(self.err(&format!("expected '{}'", byte as char)))
412        }
413    }
414
415    fn literal(&mut self, word: &str) -> Result<(), ParseError> {
416        if self.bytes[self.pos..].starts_with(word.as_bytes()) {
417            self.pos += word.len();
418            Ok(())
419        } else {
420            Err(self.err(&format!("expected '{word}'")))
421        }
422    }
423
424    fn value(&mut self) -> Result<Json, ParseError> {
425        match self.peek() {
426            None => Err(self.err("unexpected end of input")),
427            Some(b'n') => {
428                self.literal("null")?;
429                Ok(Json::Null)
430            }
431            Some(b't') => {
432                self.literal("true")?;
433                Ok(Json::Bool(true))
434            }
435            Some(b'f') => {
436                self.literal("false")?;
437                Ok(Json::Bool(false))
438            }
439            Some(b'"') => Ok(Json::Str(self.string()?)),
440            Some(b'[' | b'{') => {
441                if self.depth >= 128 {
442                    return Err(self.err("more than 128 nested containers"));
443                }
444                self.depth += 1;
445                let result = if self.peek() == Some(b'[') {
446                    self.array()
447                } else {
448                    self.object()
449                };
450                self.depth -= 1;
451                result
452            }
453            Some(_) => self.number(),
454        }
455    }
456
457    fn array(&mut self) -> Result<Json, ParseError> {
458        self.expect(b'[')?;
459        let mut items = Vec::new();
460        self.skip_ws();
461        if self.peek() == Some(b']') {
462            self.pos += 1;
463            return Ok(Json::Arr(items));
464        }
465        loop {
466            self.skip_ws();
467            items.push(self.value()?);
468            self.skip_ws();
469            match self.peek() {
470                Some(b',') => self.pos += 1,
471                Some(b']') => {
472                    self.pos += 1;
473                    return Ok(Json::Arr(items));
474                }
475                _ => return Err(self.err("expected ',' or ']'")),
476            }
477        }
478    }
479
480    fn object(&mut self) -> Result<Json, ParseError> {
481        self.expect(b'{')?;
482        let mut map = BTreeMap::new();
483        self.skip_ws();
484        if self.peek() == Some(b'}') {
485            self.pos += 1;
486            return Ok(Json::Obj(map));
487        }
488        loop {
489            self.skip_ws();
490            let key = self.string()?;
491            self.skip_ws();
492            self.expect(b':')?;
493            self.skip_ws();
494            let value = self.value()?;
495            map.insert(key, value);
496            self.skip_ws();
497            match self.peek() {
498                Some(b',') => self.pos += 1,
499                Some(b'}') => {
500                    self.pos += 1;
501                    return Ok(Json::Obj(map));
502                }
503                _ => return Err(self.err("expected ',' or '}'")),
504            }
505        }
506    }
507
508    fn string(&mut self) -> Result<String, ParseError> {
509        self.expect(b'"')?;
510        let mut out = String::new();
511        loop {
512            let byte = self.peek().ok_or_else(|| self.err("unterminated string"))?;
513            match byte {
514                b'"' => {
515                    self.pos += 1;
516                    return Ok(out);
517                }
518                b'\\' => {
519                    self.pos += 1;
520                    let esc = self.peek().ok_or_else(|| self.err("truncated escape"))?;
521                    self.pos += 1;
522                    match esc {
523                        b'"' => out.push('"'),
524                        b'\\' => out.push('\\'),
525                        b'/' => out.push('/'),
526                        b'b' => out.push('\u{08}'),
527                        b'f' => out.push('\u{0c}'),
528                        b'n' => out.push('\n'),
529                        b'r' => out.push('\r'),
530                        b't' => out.push('\t'),
531                        b'u' => {
532                            let hi = self.hex4()?;
533                            let cp = if (0xD800..0xDC00).contains(&hi) {
534                                self.expect(b'\\')?;
535                                self.expect(b'u')?;
536                                let lo = self.hex4()?;
537                                if !(0xDC00..=0xDFFF).contains(&lo) {
538                                    return Err(self.err("invalid low UTF-16 surrogate"));
539                                }
540                                0x10000
541                                    + ((u32::from(hi) - 0xD800) << 10)
542                                    + (u32::from(lo) - 0xDC00)
543                            } else {
544                                u32::from(hi)
545                            };
546                            let ch = char::from_u32(cp)
547                                .ok_or_else(|| self.err("invalid Unicode scalar"))?;
548                            out.push(ch);
549                        }
550                        _ => return Err(self.err("unknown escape sequence")),
551                    }
552                }
553                0x00..=0x1F => return Err(self.err("unescaped control character in string")),
554                _ => {
555                    // Advance over one complete UTF-8 character.
556                    let start = self.pos;
557                    let width = utf8_width(byte);
558                    self.pos = (start + width).min(self.bytes.len());
559                    match std::str::from_utf8(&self.bytes[start..self.pos]) {
560                        Ok(s) => out.push_str(s),
561                        Err(_) => return Err(self.err("invalid UTF-8 sequence")),
562                    }
563                }
564            }
565        }
566    }
567
568    fn hex4(&mut self) -> Result<u16, ParseError> {
569        if self.pos + 4 > self.bytes.len() {
570            return Err(self.err("truncated \\u escape"));
571        }
572        let slice = &self.bytes[self.pos..self.pos + 4];
573        // Rust's radix parser also accepts a leading '+', which JSON's
574        // four-hex-digit Unicode escape does not permit.
575        if !slice.iter().all(u8::is_ascii_hexdigit) {
576            return Err(self.err("non-hexadecimal \\u escape"));
577        }
578        let text =
579            std::str::from_utf8(slice).map_err(|_| self.err("non-hexadecimal \\u escape"))?;
580        let value =
581            u16::from_str_radix(text, 16).map_err(|_| self.err("non-hexadecimal \\u escape"))?;
582        self.pos += 4;
583        Ok(value)
584    }
585
586    fn number(&mut self) -> Result<Json, ParseError> {
587        let start = self.pos;
588        if self.peek() == Some(b'-') {
589            self.pos += 1;
590        }
591        match self.peek() {
592            Some(b'0') => {
593                self.pos += 1;
594                if matches!(self.peek(), Some(b'0'..=b'9')) {
595                    return Err(self.err("leading zero in number"));
596                }
597            }
598            Some(b'1'..=b'9') => {
599                self.digits()?;
600            }
601            _ => return Err(self.err("expected a number")),
602        }
603        if self.peek() == Some(b'.') {
604            self.pos += 1;
605            self.digits()?;
606        }
607        if matches!(self.peek(), Some(b'e' | b'E')) {
608            self.pos += 1;
609            if matches!(self.peek(), Some(b'+' | b'-')) {
610                self.pos += 1;
611            }
612            self.digits()?;
613        }
614        let text = std::str::from_utf8(&self.bytes[start..self.pos])
615            .map_err(|_| self.err("malformed number"))?;
616        let number = text.parse::<f64>().map_err(|_| ParseError {
617            message: "malformed number".into(),
618            offset: start,
619        })?;
620        if !number.is_finite() {
621            return Err(self.err("number is outside the finite range"));
622        }
623        Ok(Json::Num(number))
624    }
625
626    fn digits(&mut self) -> Result<(), ParseError> {
627        let start = self.pos;
628        while matches!(self.peek(), Some(b'0'..=b'9')) {
629            self.pos += 1;
630        }
631        if self.pos == start {
632            return Err(self.err("expected a decimal digit"));
633        }
634        Ok(())
635    }
636}
637
638fn utf8_width(first: u8) -> usize {
639    match first {
640        0x00..=0x7F => 1,
641        0xC0..=0xDF => 2,
642        0xE0..=0xEF => 3,
643        0xF0..=0xF7 => 4,
644        _ => 1,
645    }
646}
647
648#[cfg(test)]
649#[path = "json_tests.rs"]
650mod tests;