Skip to main content

hl7_2/
json.rs

1//! A small hand-written JSON reader, used to load dictionaries.
2//!
3//! Dictionaries — the bundled ones in `schemas/` and the ones callers write
4//! themselves for schema mode — are JSON, so something has to read JSON.
5//! Doing it here, in about the space a dependency declaration would take,
6//! keeps this crate's runtime dependency list at exactly one entry
7//! (`er7`, which itself has none), which is worth more in a domain where
8//! dependency trees get audited than the few hundred lines it costs. The
9//! sibling `hl7-v2-from-er7-into-json` crate hand-rolls its JSON *writer*
10//! for the same reason; this is the mirror of it.
11//!
12//! The reader is deliberately plain: it accepts RFC 8259 JSON, keeps object
13//! members in file order (dictionaries are read, diffed, and eyeballed by
14//! people, so order is worth preserving), and reports the byte offset of a
15//! syntax error so a typo in a 1,300-line dictionary is findable.
16
17use std::fmt;
18
19/// A parsed JSON value.
20///
21/// Object members are a `Vec` rather than a map so that document order
22/// survives; dictionaries are small enough that lookup by scan is not worth
23/// a `BTreeMap`'s allocation per object.
24#[derive(Debug, Clone, PartialEq)]
25pub enum Value {
26    /// `null`.
27    Null,
28    /// `true` or `false`.
29    Bool(bool),
30    /// Any JSON number, held as `f64`.
31    Number(f64),
32    /// A string, with escape sequences already resolved.
33    String(String),
34    /// An array, in order.
35    Array(Vec<Value>),
36    /// An object: members in the order they appeared.
37    Object(Vec<(String, Value)>),
38}
39
40impl Value {
41    /// The member named `key`, if this is an object that has one.
42    ///
43    /// A duplicate key returns the first occurrence, matching the "first
44    /// wins" reading most JSON tooling settles on.
45    #[must_use]
46    pub fn get(&self, key: &str) -> Option<&Value> {
47        match self {
48            Value::Object(members) => members.iter().find(|(k, _)| k == key).map(|(_, v)| v),
49            _ => None,
50        }
51    }
52
53    /// The string, if this is a string.
54    #[must_use]
55    pub fn as_str(&self) -> Option<&str> {
56        match self {
57            Value::String(s) => Some(s),
58            _ => None,
59        }
60    }
61
62    /// The members, if this is an object.
63    #[must_use]
64    pub fn as_object(&self) -> Option<&[(String, Value)]> {
65        match self {
66            Value::Object(members) => Some(members),
67            _ => None,
68        }
69    }
70
71    /// The elements, if this is an array.
72    #[must_use]
73    pub fn as_array(&self) -> Option<&[Value]> {
74        match self {
75            Value::Array(items) => Some(items),
76            _ => None,
77        }
78    }
79
80    /// The boolean, if this is one.
81    #[must_use]
82    pub fn as_bool(&self) -> Option<bool> {
83        match self {
84            Value::Bool(b) => Some(*b),
85            _ => None,
86        }
87    }
88
89    /// True when this is `null`.
90    #[must_use]
91    pub fn is_null(&self) -> bool {
92        matches!(self, Value::Null)
93    }
94
95    /// The name of this value's kind, for error messages.
96    #[must_use]
97    pub fn kind(&self) -> &'static str {
98        match self {
99            Value::Null => "null",
100            Value::Bool(_) => "boolean",
101            Value::Number(_) => "number",
102            Value::String(_) => "string",
103            Value::Array(_) => "array",
104            Value::Object(_) => "object",
105        }
106    }
107}
108
109/// A JSON syntax error: what was wrong, and where.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct Error {
112    /// What the reader expected, or what it found instead.
113    pub detail: String,
114    /// Byte offset into the input where the problem was noticed.
115    pub offset: usize,
116}
117
118impl fmt::Display for Error {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(f, "invalid JSON at byte {}: {}", self.offset, self.detail)
121    }
122}
123
124impl std::error::Error for Error {}
125
126/// Read a complete JSON document. Trailing content after the top-level
127/// value is an error, so a truncated or double-pasted file is caught here
128/// rather than silently half-read.
129/// # Errors
130///
131/// [`Error`] when the text is not valid JSON, with where parsing gave up.
132pub fn parse(text: &str) -> Result<Value, Error> {
133    let mut reader = Reader {
134        bytes: text.as_bytes(),
135        pos: 0,
136    };
137    reader.skip_whitespace();
138    let value = reader.value()?;
139    reader.skip_whitespace();
140    if reader.pos != reader.bytes.len() {
141        return Err(reader.error("unexpected trailing content"));
142    }
143    Ok(value)
144}
145
146struct Reader<'a> {
147    bytes: &'a [u8],
148    pos: usize,
149}
150
151impl Reader<'_> {
152    fn error(&self, detail: &str) -> Error {
153        Error {
154            detail: detail.to_string(),
155            offset: self.pos,
156        }
157    }
158
159    fn peek(&self) -> Option<u8> {
160        self.bytes.get(self.pos).copied()
161    }
162
163    fn skip_whitespace(&mut self) {
164        while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
165            self.pos += 1;
166        }
167    }
168
169    /// Consume `word` if it is next, reporting a useful error if not.
170    fn literal(&mut self, word: &str, value: Value) -> Result<Value, Error> {
171        if self.bytes[self.pos..].starts_with(word.as_bytes()) {
172            self.pos += word.len();
173            Ok(value)
174        } else {
175            Err(self.error(&format!("expected `{word}`")))
176        }
177    }
178
179    fn value(&mut self) -> Result<Value, Error> {
180        match self.peek() {
181            None => Err(self.error("expected a value")),
182            Some(b'{') => self.object(),
183            Some(b'[') => self.array(),
184            Some(b'"') => Ok(Value::String(self.string()?)),
185            Some(b't') => self.literal("true", Value::Bool(true)),
186            Some(b'f') => self.literal("false", Value::Bool(false)),
187            Some(b'n') => self.literal("null", Value::Null),
188            Some(_) => self.number(),
189        }
190    }
191
192    fn object(&mut self) -> Result<Value, Error> {
193        self.pos += 1; // `{`
194        let mut members = Vec::new();
195        self.skip_whitespace();
196        if self.peek() == Some(b'}') {
197            self.pos += 1;
198            return Ok(Value::Object(members));
199        }
200        loop {
201            self.skip_whitespace();
202            if self.peek() != Some(b'"') {
203                return Err(self.error("expected a member name"));
204            }
205            let name = self.string()?;
206            self.skip_whitespace();
207            if self.peek() != Some(b':') {
208                return Err(self.error("expected `:` after a member name"));
209            }
210            self.pos += 1;
211            self.skip_whitespace();
212            members.push((name, self.value()?));
213            self.skip_whitespace();
214            match self.peek() {
215                Some(b',') => self.pos += 1,
216                Some(b'}') => {
217                    self.pos += 1;
218                    return Ok(Value::Object(members));
219                }
220                _ => return Err(self.error("expected `,` or `}`")),
221            }
222        }
223    }
224
225    fn array(&mut self) -> Result<Value, Error> {
226        self.pos += 1; // `[`
227        let mut items = Vec::new();
228        self.skip_whitespace();
229        if self.peek() == Some(b']') {
230            self.pos += 1;
231            return Ok(Value::Array(items));
232        }
233        loop {
234            self.skip_whitespace();
235            items.push(self.value()?);
236            self.skip_whitespace();
237            match self.peek() {
238                Some(b',') => self.pos += 1,
239                Some(b']') => {
240                    self.pos += 1;
241                    return Ok(Value::Array(items));
242                }
243                _ => return Err(self.error("expected `,` or `]`")),
244            }
245        }
246    }
247
248    fn string(&mut self) -> Result<String, Error> {
249        self.pos += 1; // opening quote
250        let mut out = String::new();
251        loop {
252            let Some(byte) = self.peek() else {
253                return Err(self.error("unterminated string"));
254            };
255            match byte {
256                b'"' => {
257                    self.pos += 1;
258                    return Ok(out);
259                }
260                b'\\' => {
261                    self.pos += 1;
262                    self.escape(&mut out)?;
263                }
264                0x00..=0x1f => return Err(self.error("unescaped control character in string")),
265                _ => {
266                    // Multi-byte UTF-8 passes through whole: find the end of
267                    // this character in the original text rather than
268                    // pushing bytes one at a time.
269                    let start = self.pos;
270                    self.pos += 1;
271                    while matches!(self.peek(), Some(b) if b & 0xc0 == 0x80) {
272                        self.pos += 1;
273                    }
274                    match std::str::from_utf8(&self.bytes[start..self.pos]) {
275                        Ok(text) => out.push_str(text),
276                        Err(_) => return Err(self.error("invalid UTF-8 in string")),
277                    }
278                }
279            }
280        }
281    }
282
283    fn escape(&mut self, out: &mut String) -> Result<(), Error> {
284        let Some(byte) = self.peek() else {
285            return Err(self.error("unterminated escape sequence"));
286        };
287        self.pos += 1;
288        out.push(match byte {
289            b'"' => '"',
290            b'\\' => '\\',
291            b'/' => '/',
292            b'b' => '\u{8}',
293            b'f' => '\u{c}',
294            b'n' => '\n',
295            b'r' => '\r',
296            b't' => '\t',
297            b'u' => return self.unicode_escape(out),
298            _ => return Err(self.error("unknown escape sequence")),
299        });
300        Ok(())
301    }
302
303    /// Resolve `\uXXXX`, joining a surrogate pair when one follows.
304    fn unicode_escape(&mut self, out: &mut String) -> Result<(), Error> {
305        let high = self.hex4()?;
306        let scalar = if (0xd800..0xdc00).contains(&high) {
307            if !self.bytes[self.pos..].starts_with(b"\\u") {
308                return Err(self.error("lone high surrogate"));
309            }
310            self.pos += 2;
311            let low = self.hex4()?;
312            if !(0xdc00..0xe000).contains(&low) {
313                return Err(self.error("expected a low surrogate"));
314            }
315            0x10000 + ((high - 0xd800) << 10) + (low - 0xdc00)
316        } else if (0xdc00..0xe000).contains(&high) {
317            return Err(self.error("lone low surrogate"));
318        } else {
319            high
320        };
321        match char::from_u32(scalar) {
322            Some(c) => out.push(c),
323            None => return Err(self.error("escape is not a Unicode scalar value")),
324        }
325        Ok(())
326    }
327
328    fn hex4(&mut self) -> Result<u32, Error> {
329        let end = self.pos + 4;
330        if end > self.bytes.len() {
331            return Err(self.error("truncated `\\u` escape"));
332        }
333        let mut value = 0;
334        for &byte in &self.bytes[self.pos..end] {
335            let digit = match byte {
336                b'0'..=b'9' => u32::from(byte - b'0'),
337                b'a'..=b'f' => u32::from(byte - b'a') + 10,
338                b'A'..=b'F' => u32::from(byte - b'A') + 10,
339                _ => return Err(self.error("`\\u` escape needs four hex digits")),
340            };
341            value = value * 16 + digit;
342        }
343        self.pos = end;
344        Ok(value)
345    }
346
347    fn number(&mut self) -> Result<Value, Error> {
348        let start = self.pos;
349        if self.peek() == Some(b'-') {
350            self.pos += 1;
351        }
352        while matches!(
353            self.peek(),
354            Some(b'0'..=b'9' | b'.' | b'e' | b'E' | b'+' | b'-')
355        ) {
356            self.pos += 1;
357        }
358        let text = std::str::from_utf8(&self.bytes[start..self.pos]).unwrap_or("");
359        if let Ok(number) = text.parse::<f64>() {
360            Ok(Value::Number(number))
361        } else {
362            self.pos = start;
363            Err(self.error("expected a value"))
364        }
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn reads_the_shapes_a_dictionary_uses() {
374        let value =
375            parse(r#"{"types": {"XPN": ["FN", "ST"]}, "n": 2.5, "ok": true, "x": null}"#).unwrap();
376        assert_eq!(
377            value
378                .get("types")
379                .unwrap()
380                .get("XPN")
381                .unwrap()
382                .as_array()
383                .unwrap()[0]
384                .as_str(),
385            Some("FN")
386        );
387        assert_eq!(value.get("n"), Some(&Value::Number(2.5)));
388        assert_eq!(value.get("ok").unwrap().as_bool(), Some(true));
389        assert!(value.get("x").unwrap().is_null());
390        assert_eq!(value.get("missing"), None);
391    }
392
393    #[test]
394    fn keeps_object_order() {
395        let value = parse(r#"{"b": 1, "a": 2}"#).unwrap();
396        let names: Vec<&str> = value
397            .as_object()
398            .unwrap()
399            .iter()
400            .map(|(name, _)| name.as_str())
401            .collect();
402        assert_eq!(names, ["b", "a"]);
403    }
404
405    #[test]
406    fn resolves_escapes_including_surrogate_pairs() {
407        assert_eq!(
408            parse(r#""aé😀\n\"\\""#).unwrap().as_str(),
409            Some("aé😀\n\"\\")
410        );
411        assert_eq!(parse("\"caf\u{e9}\"").unwrap().as_str(), Some("café"));
412    }
413
414    #[test]
415    fn reports_where_the_problem_is() {
416        // A dictionary is long; "somewhere" is not a useful answer.
417        let error = parse(r#"{"a": 1,}"#).unwrap_err();
418        assert_eq!(error.offset, 8);
419        assert!(error.to_string().contains("byte 8"), "{error}");
420        assert!(parse(r#"{"a": 1} {"b": 2}"#).is_err());
421        assert!(parse(r#"{"a" 1}"#).is_err());
422        assert!(parse(r#""unterminated"#).is_err());
423        assert!(parse("").is_err());
424    }
425}