Skip to main content

doom_fish_utils/
json.rs

1#![allow(clippy::missing_errors_doc)]
2
3use std::collections::BTreeMap;
4use std::fmt::{self, Write};
5
6#[derive(Debug, Clone, PartialEq)]
7pub enum JsonValue {
8    Null,
9    Bool(bool),
10    Integer(i64),
11    Float(f64),
12    String(String),
13    Array(Vec<Self>),
14    Object(BTreeMap<String, Self>),
15}
16
17impl JsonValue {
18    pub fn field<T: FromJson>(&self, key: &str) -> Result<T, JsonError> {
19        let Self::Object(object) = self else {
20            return Err(JsonError("expected an object".into()));
21        };
22        object.get(key).map_or_else(
23            || T::from_json(&Self::Null).map_err(|_| JsonError(format!("missing field `{key}`"))),
24            T::from_json,
25        )
26    }
27}
28
29impl fmt::Display for JsonValue {
30    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            Self::Bool(value) => write!(formatter, "{value}"),
33            Self::Integer(value) => write!(formatter, "{value}"),
34            Self::Float(value) if value.is_finite() => write!(formatter, "{value:?}"),
35            Self::Null | Self::Float(_) => formatter.write_str("null"),
36            Self::String(value) => write_string(value, formatter),
37            Self::Array(values) => {
38                formatter.write_str("[")?;
39                for (index, value) in values.iter().enumerate() {
40                    if index != 0 {
41                        formatter.write_str(",")?;
42                    }
43                    write!(formatter, "{value}")?;
44                }
45                formatter.write_str("]")
46            }
47            Self::Object(values) => {
48                formatter.write_str("{")?;
49                for (index, (key, value)) in values.iter().enumerate() {
50                    if index != 0 {
51                        formatter.write_str(",")?;
52                    }
53                    write_string(key, formatter)?;
54                    write!(formatter, ":{value}")?;
55                }
56                formatter.write_str("}")
57            }
58        }
59    }
60}
61
62fn write_string(text: &str, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63    formatter.write_str("\"")?;
64    for character in text.chars() {
65        match character {
66            '"' => formatter.write_str("\\\"")?,
67            '\\' => formatter.write_str("\\\\")?,
68            '\u{08}' => formatter.write_str("\\b")?,
69            '\u{0c}' => formatter.write_str("\\f")?,
70            '\n' => formatter.write_str("\\n")?,
71            '\r' => formatter.write_str("\\r")?,
72            '\t' => formatter.write_str("\\t")?,
73            '\u{00}'..='\u{1f}' => write!(formatter, "\\u{:04x}", u32::from(character))?,
74            _ => formatter.write_char(character)?,
75        }
76    }
77    formatter.write_str("\"")
78}
79
80#[derive(Debug)]
81pub struct JsonError(String);
82
83impl fmt::Display for JsonError {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        formatter.write_str(&self.0)
86    }
87}
88
89impl std::error::Error for JsonError {}
90
91pub trait FromJson: Sized {
92    fn from_json(value: &JsonValue) -> Result<Self, JsonError>;
93}
94
95impl FromJson for JsonValue {
96    fn from_json(value: &JsonValue) -> Result<Self, JsonError> {
97        Ok(value.clone())
98    }
99}
100
101impl FromJson for bool {
102    fn from_json(value: &JsonValue) -> Result<Self, JsonError> {
103        match value {
104            JsonValue::Bool(value) => Ok(*value),
105            _ => Err(JsonError("expected a boolean".into())),
106        }
107    }
108}
109
110macro_rules! integer_from_json {
111    ($($type:ty),+ $(,)?) => {
112        $(
113            impl FromJson for $type {
114                fn from_json(value: &JsonValue) -> Result<Self, JsonError> {
115                    match value {
116                        JsonValue::Integer(value) => Self::try_from(*value).map_err(|_| {
117                            JsonError(concat!("integer out of range for ", stringify!($type)).into())
118                        }),
119                        _ => Err(JsonError(concat!("expected an integer for ", stringify!($type)).into())),
120                    }
121                }
122            }
123        )+
124    };
125}
126
127integer_from_json!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize);
128
129impl FromJson for f64 {
130    #[allow(clippy::cast_precision_loss)]
131    fn from_json(value: &JsonValue) -> Result<Self, JsonError> {
132        match value {
133            JsonValue::Integer(value) => Ok(*value as Self),
134            JsonValue::Float(value) => Ok(*value),
135            _ => Err(JsonError("expected a number".into())),
136        }
137    }
138}
139
140impl FromJson for f32 {
141    #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)]
142    fn from_json(value: &JsonValue) -> Result<Self, JsonError> {
143        match value {
144            JsonValue::Integer(value) => Ok(*value as Self),
145            JsonValue::Float(value) => Ok(*value as Self),
146            _ => Err(JsonError("expected a number".into())),
147        }
148    }
149}
150
151impl FromJson for String {
152    fn from_json(value: &JsonValue) -> Result<Self, JsonError> {
153        match value {
154            JsonValue::String(value) => Ok(value.clone()),
155            _ => Err(JsonError("expected a string".into())),
156        }
157    }
158}
159
160impl<T: FromJson> FromJson for Option<T> {
161    fn from_json(value: &JsonValue) -> Result<Self, JsonError> {
162        match value {
163            JsonValue::Null => Ok(None),
164            _ => T::from_json(value).map(Some),
165        }
166    }
167}
168
169impl<T: FromJson> FromJson for Vec<T> {
170    fn from_json(value: &JsonValue) -> Result<Self, JsonError> {
171        match value {
172            JsonValue::Array(values) => values.iter().map(T::from_json).collect(),
173            _ => Err(JsonError("expected an array".into())),
174        }
175    }
176}
177
178impl<T: FromJson> FromJson for BTreeMap<String, T> {
179    fn from_json(value: &JsonValue) -> Result<Self, JsonError> {
180        match value {
181            JsonValue::Object(values) => values
182                .iter()
183                .map(|(key, value)| T::from_json(value).map(|value| (key.clone(), value)))
184                .collect(),
185            _ => Err(JsonError("expected an object".into())),
186        }
187    }
188}
189
190pub fn parse(text: &str) -> Result<JsonValue, JsonError> {
191    let mut reader = Reader { text, position: 0 };
192    let value = reader.value(0)?;
193    reader.whitespace();
194    if reader.position != text.len() {
195        return Err(reader.error("unexpected trailing data"));
196    }
197    Ok(value)
198}
199
200struct Reader<'a> {
201    text: &'a str,
202    position: usize,
203}
204
205impl Reader<'_> {
206    fn error(&self, message: &str) -> JsonError {
207        JsonError(format!("{message} at byte {}", self.position))
208    }
209
210    fn peek(&self) -> Option<u8> {
211        self.text.as_bytes().get(self.position).copied()
212    }
213
214    fn consume(&mut self, byte: u8) -> bool {
215        if self.peek() == Some(byte) {
216            self.position += 1;
217            true
218        } else {
219            false
220        }
221    }
222
223    fn whitespace(&mut self) {
224        while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
225            self.position += 1;
226        }
227    }
228
229    fn value(&mut self, depth: usize) -> Result<JsonValue, JsonError> {
230        self.whitespace();
231        match self.peek() {
232            Some(b'{' | b'[') if depth >= 128 => Err(self.error("nesting limit exceeded")),
233            Some(b'{') => self.object(depth + 1),
234            Some(b'[') => self.array(depth + 1),
235            Some(b'"') => self.string().map(JsonValue::String),
236            Some(b'-' | b'0'..=b'9') => self.number(),
237            Some(b't') if self.text[self.position..].starts_with("true") => {
238                self.position += 4;
239                Ok(JsonValue::Bool(true))
240            }
241            Some(b'f') if self.text[self.position..].starts_with("false") => {
242                self.position += 5;
243                Ok(JsonValue::Bool(false))
244            }
245            Some(b'n') if self.text[self.position..].starts_with("null") => {
246                self.position += 4;
247                Ok(JsonValue::Null)
248            }
249            _ => Err(self.error("expected a JSON value")),
250        }
251    }
252
253    fn object(&mut self, depth: usize) -> Result<JsonValue, JsonError> {
254        self.position += 1;
255        let mut values = BTreeMap::new();
256        self.whitespace();
257        if self.consume(b'}') {
258            return Ok(JsonValue::Object(values));
259        }
260        loop {
261            if self.peek() != Some(b'"') {
262                return Err(self.error("expected an object key"));
263            }
264            let key = self.string()?;
265            self.whitespace();
266            if !self.consume(b':') {
267                return Err(self.error("expected `:`"));
268            }
269            let value = self.value(depth)?;
270            values.insert(key, value);
271            self.whitespace();
272            if self.consume(b'}') {
273                return Ok(JsonValue::Object(values));
274            }
275            if !self.consume(b',') {
276                return Err(self.error("expected `,` or `}`"));
277            }
278            self.whitespace();
279        }
280    }
281
282    fn array(&mut self, depth: usize) -> Result<JsonValue, JsonError> {
283        self.position += 1;
284        let mut values = Vec::new();
285        self.whitespace();
286        if self.consume(b']') {
287            return Ok(JsonValue::Array(values));
288        }
289        loop {
290            values.push(self.value(depth)?);
291            self.whitespace();
292            if self.consume(b']') {
293                return Ok(JsonValue::Array(values));
294            }
295            if !self.consume(b',') {
296                return Err(self.error("expected `,` or `]`"));
297            }
298        }
299    }
300
301    fn string(&mut self) -> Result<String, JsonError> {
302        self.position += 1;
303        let mut value = String::new();
304        let mut start = self.position;
305        loop {
306            match self.peek() {
307                Some(b'"') => {
308                    value.push_str(&self.text[start..self.position]);
309                    self.position += 1;
310                    return Ok(value);
311                }
312                Some(b'\\') => {
313                    value.push_str(&self.text[start..self.position]);
314                    self.position += 1;
315                    let escaped = match self.peek() {
316                        Some(b'"') => '"',
317                        Some(b'\\') => '\\',
318                        Some(b'/') => '/',
319                        Some(b'b') => '\u{08}',
320                        Some(b'f') => '\u{0c}',
321                        Some(b'n') => '\n',
322                        Some(b'r') => '\r',
323                        Some(b't') => '\t',
324                        Some(b'u') => {
325                            self.position += 1;
326                            let first = self.hex_quad()?;
327                            let code = if (0xd800..=0xdbff).contains(&first) {
328                                if !self.consume(b'\\') || !self.consume(b'u') {
329                                    return Err(self.error("expected a low surrogate"));
330                                }
331                                let second = self.hex_quad()?;
332                                if !(0xdc00..=0xdfff).contains(&second) {
333                                    return Err(self.error("invalid low surrogate"));
334                                }
335                                0x10000 + ((first - 0xd800) << 10) + second - 0xdc00
336                            } else {
337                                first
338                            };
339                            let character = char::from_u32(code)
340                                .ok_or_else(|| self.error("invalid Unicode escape"))?;
341                            value.push(character);
342                            start = self.position;
343                            continue;
344                        }
345                        _ => return Err(self.error("invalid string escape")),
346                    };
347                    value.push(escaped);
348                    self.position += 1;
349                    start = self.position;
350                }
351                Some(0..=0x1f) => return Err(self.error("unescaped control character")),
352                Some(_) => self.position += 1,
353                None => return Err(self.error("unterminated string")),
354            }
355        }
356    }
357
358    fn hex_quad(&mut self) -> Result<u32, JsonError> {
359        let mut value = 0;
360        for _ in 0..4 {
361            let digit = match self.peek() {
362                Some(byte @ b'0'..=b'9') => byte - b'0',
363                Some(byte @ b'a'..=b'f') => byte - b'a' + 10,
364                Some(byte @ b'A'..=b'F') => byte - b'A' + 10,
365                _ => return Err(self.error("invalid Unicode escape")),
366            };
367            value = value * 16 + u32::from(digit);
368            self.position += 1;
369        }
370        Ok(value)
371    }
372
373    fn number(&mut self) -> Result<JsonValue, JsonError> {
374        let start = self.position;
375        self.consume(b'-');
376        match self.peek() {
377            Some(b'0') => self.position += 1,
378            Some(b'1'..=b'9') => {
379                while matches!(self.peek(), Some(b'0'..=b'9')) {
380                    self.position += 1;
381                }
382            }
383            _ => return Err(self.error("expected a digit")),
384        }
385        let mut float = false;
386        if self.consume(b'.') {
387            float = true;
388            let fraction_start = self.position;
389            while matches!(self.peek(), Some(b'0'..=b'9')) {
390                self.position += 1;
391            }
392            if self.position == fraction_start {
393                return Err(self.error("expected a fraction digit"));
394            }
395        }
396        if self.consume(b'e') || self.consume(b'E') {
397            float = true;
398            if !self.consume(b'+') {
399                self.consume(b'-');
400            }
401            let exponent_start = self.position;
402            while matches!(self.peek(), Some(b'0'..=b'9')) {
403                self.position += 1;
404            }
405            if self.position == exponent_start {
406                return Err(self.error("expected an exponent digit"));
407            }
408        }
409        let token = &self.text[start..self.position];
410        if !float && token != "-0" {
411            if let Ok(value) = token.parse::<i64>() {
412                return Ok(JsonValue::Integer(value));
413            }
414        }
415        let value = token
416            .parse::<f64>()
417            .map_err(|_| self.error("invalid number"))?;
418        if !value.is_finite() {
419            return Err(self.error("number out of range"));
420        }
421        Ok(JsonValue::Float(value))
422    }
423}