Skip to main content

tanzim_parse/
json.rs

1//! JSON parser (`json` feature).
2//!
3//! **Format:** `json`
4//!
5//! # Behaviour
6//!
7//! - Parses standard JSON with source spans. Objects become maps, arrays become lists, and
8//!   strings/numbers/booleans become the matching scalar values; integers and floats are
9//!   distinguished.
10//! - Every node — root, map values, and list items — carries its span as a [`Location`]
11//!   (line/column); for single-line input the line/column are omitted.
12//! - JSON `null` is rejected with [`Error::UnsupportedNull`], since the config model has no null.
13//!   Non-UTF-8 input fails with [`Error::InvalidUtf8`], and any syntax error becomes
14//!   [`Error::Parse`] with the failing position.
15//! - [`is_format_supported`](crate::Parse::is_format_supported) returns `Some(true)` when
16//!   the bytes parse as JSON, else `Some(false)`.
17//!
18//! # Example
19//!
20//! ```
21//! use tanzim_parse::{Parse, json::Json};
22//! use tanzim_source::SourceBuilder;
23//!
24//! let source = SourceBuilder::new()
25//!     .with_source("file")
26//!     .with_resource("config.json")
27//!     .build()
28//!     .unwrap();
29//! let value = Json::new()
30//!     .parse(&source, br#"{"host":"127.0.0.1"}"#)
31//!     .unwrap();
32//! assert_eq!(
33//!     value.value.as_map().unwrap().get("host").unwrap().value.as_string().unwrap(),
34//!     "127.0.0.1"
35//! );
36//! ```
37
38use crate::span::is_single_line;
39use crate::{Parse, Source};
40use cfg_if::cfg_if;
41use spanned_json_parser::value::Value as JsonValue;
42use spanned_json_parser::{Position, parse};
43use tanzim_value::{Error, LocatedValue, Location, Map, Value};
44
45/// Parser for the `json` format: standard JSON into a source-located value tree.
46///
47/// Objects, arrays, and scalars map to the value tree with a per-node span [`Location`]; JSON
48/// `null` is rejected with [`Error::UnsupportedNull`]. Stateless — construct with [`Json::new`].
49///
50/// ```
51/// use tanzim_parse::{Parse, json::Json};
52/// use tanzim_source::SourceBuilder;
53///
54/// let source = SourceBuilder::new()
55///     .with_source("file")
56///     .with_resource("config.json")
57///     .build()
58///     .unwrap();
59/// let value = Json::new().parse(&source, br#"{"port":8080}"#).unwrap();
60/// let port = value.value.as_map().unwrap().get("port").unwrap();
61/// assert_eq!(port.value.as_int().unwrap(), 8080);
62/// ```
63#[derive(Clone, Copy, Default)]
64pub struct Json;
65
66impl Json {
67    /// Create a JSON parser.
68    pub fn new() -> Self {
69        Self
70    }
71}
72
73impl Parse for Json {
74    fn name(&self) -> &str {
75        "JSON"
76    }
77
78    fn supported_format_list(&self) -> Vec<String> {
79        vec!["json".into()]
80    }
81
82    fn parse(&self, src: &Source, bytes: &[u8]) -> Result<LocatedValue, Error> {
83        let source = src.source();
84        let resource = src.resource();
85        cfg_if! {
86            if #[cfg(feature = "tracing")] {
87                tracing::debug!(msg = "Parsing JSON configuration", source = source, resource = resource, bytes = bytes.len());
88            } else if #[cfg(feature = "logging")] {
89                log::debug!("msg=\"Parsing JSON configuration\" source={source} resource={resource} bytes={}", bytes.len());
90            }
91        }
92        let text = match std::str::from_utf8(bytes) {
93            Ok(value) => value,
94            Err(_) => {
95                return Err(Error::InvalidUtf8 {
96                    location: Location::at(source, resource, None, None, None),
97                });
98            }
99        };
100        let single_line = is_single_line(bytes);
101        let parsed = match parse(text) {
102            Ok(value) => value,
103            Err(error) => {
104                return Err(Error::Parse {
105                    text: text.to_string(),
106                    location: Some(location_from_position(
107                        source,
108                        resource,
109                        single_line,
110                        &error.start,
111                        Some(&error.end),
112                    )),
113                    message: format!("{:?}", error.kind),
114                });
115            }
116        };
117        let location = location_from_position(
118            source,
119            resource,
120            single_line,
121            &parsed.start,
122            Some(&parsed.end),
123        );
124        let result = convert_value(
125            source,
126            resource,
127            text,
128            single_line,
129            parsed.value,
130            &parsed.start,
131            location,
132        );
133        if result.is_ok() {
134            cfg_if! {
135                if #[cfg(feature = "tracing")] {
136                    tracing::trace!(msg = "Parsed JSON configuration", source = source, resource = resource);
137                } else if #[cfg(feature = "logging")] {
138                    log::trace!("msg=\"Parsed JSON configuration\" source={source} resource={resource}");
139                }
140            }
141        }
142        result
143    }
144
145    fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
146        match std::str::from_utf8(bytes) {
147            Ok(text) => Some(parse(text).is_ok()),
148            Err(_) => Some(false),
149        }
150    }
151}
152
153/// Serialize a [`Value`] tree into pretty-printed JSON (2-space indent).
154///
155/// Accepts a [`Value`], `&Value`, [`LocatedValue`], or `&LocatedValue`. `source` is
156/// accepted for signature symmetry with [`Parse::parse`] but is unused here.
157///
158/// ```
159/// use tanzim_parse::json::unparse;
160/// use tanzim_source::SourceBuilder;
161/// use tanzim_value::{Map, LocatedValue, Location, Value};
162///
163/// let source = SourceBuilder::new().with_source("file").build().unwrap();
164/// let mut map = Map::new();
165/// map.insert("port".into(), LocatedValue {
166///     value: Value::Int(8080),
167///     location: Location::at("file", "", None, None, None),
168/// });
169/// let text = unparse(&source, Value::Map(map)).unwrap();
170/// assert_eq!(text, "{\n  \"port\": 8080\n}");
171/// ```
172pub fn unparse<V: AsRef<Value>>(
173    _source: &Source,
174    value: V,
175) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
176    let mut out = String::new();
177    write_json(&mut out, value.as_ref(), 0)?;
178    Ok(out)
179}
180
181fn write_json(
182    out: &mut String,
183    value: &Value,
184    indent: usize,
185) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
186    match value {
187        Value::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
188        Value::Int(value) => out.push_str(&value.to_string()),
189        Value::Float(value) => {
190            if !value.is_finite() {
191                return Err(format!("cannot serialize non-finite float {value} as JSON").into());
192            }
193            out.push_str(&format!("{value:?}"));
194        }
195        Value::String(value) => write_json_string(out, value),
196        Value::List(values) => {
197            if values.is_empty() {
198                out.push_str("[]");
199                return Ok(());
200            }
201            out.push_str("[\n");
202            for (index, item) in values.iter().enumerate() {
203                push_indent(out, indent + 1);
204                write_json(out, &item.value, indent + 1)?;
205                if index + 1 < values.len() {
206                    out.push(',');
207                }
208                out.push('\n');
209            }
210            push_indent(out, indent);
211            out.push(']');
212        }
213        Value::Map(map) => {
214            let entries = map.entries();
215            if entries.is_empty() {
216                out.push_str("{}");
217                return Ok(());
218            }
219            out.push_str("{\n");
220            for (index, (key, item)) in entries.iter().enumerate() {
221                push_indent(out, indent + 1);
222                write_json_string(out, key);
223                out.push_str(": ");
224                write_json(out, &item.value, indent + 1)?;
225                if index + 1 < entries.len() {
226                    out.push(',');
227                }
228                out.push('\n');
229            }
230            push_indent(out, indent);
231            out.push('}');
232        }
233    }
234    Ok(())
235}
236
237fn push_indent(out: &mut String, indent: usize) {
238    for _ in 0..indent {
239        out.push_str("  ");
240    }
241}
242
243fn write_json_string(out: &mut String, value: &str) {
244    out.push('"');
245    for ch in value.chars() {
246        match ch {
247            '"' => out.push_str("\\\""),
248            '\\' => out.push_str("\\\\"),
249            '\n' => out.push_str("\\n"),
250            '\r' => out.push_str("\\r"),
251            '\t' => out.push_str("\\t"),
252            control if (control as u32) < 0x20 => {
253                out.push_str(&format!("\\u{:04x}", control as u32));
254            }
255            other => out.push(other),
256        }
257    }
258    out.push('"');
259}
260
261fn convert_value(
262    source: &str,
263    resource: &str,
264    text: &str,
265    single_line: bool,
266    value: JsonValue,
267    _start: &Position,
268    location: Location,
269) -> Result<LocatedValue, Error> {
270    match value {
271        JsonValue::Null => Err(Error::UnsupportedNull {
272            text: text.to_string(),
273            location,
274        }),
275        JsonValue::Bool(value) => Ok(LocatedValue {
276            value: Value::Bool(value),
277            location,
278        }),
279        JsonValue::Number(number) => match number {
280            spanned_json_parser::value::Number::PosInt(value) => Ok(LocatedValue {
281                value: Value::Int(value as isize),
282                location,
283            }),
284            spanned_json_parser::value::Number::NegInt(value) => Ok(LocatedValue {
285                value: Value::Int(value as isize),
286                location,
287            }),
288            spanned_json_parser::value::Number::Float(value) => Ok(LocatedValue {
289                value: Value::Float(value),
290                location,
291            }),
292        },
293        JsonValue::String(value) => Ok(LocatedValue {
294            value: Value::String(value),
295            location,
296        }),
297        JsonValue::Array(values) => {
298            let mut list = Vec::new();
299            for item in &values {
300                let item_location = location_from_position(
301                    source,
302                    resource,
303                    single_line,
304                    &item.start,
305                    Some(&item.end),
306                );
307                let converted = convert_value(
308                    source,
309                    resource,
310                    text,
311                    single_line,
312                    item.value.clone(),
313                    &item.start,
314                    item_location,
315                )?;
316                list.push(converted);
317            }
318            Ok(LocatedValue {
319                value: Value::List(list),
320                location,
321            })
322        }
323        JsonValue::Object(values) => {
324            let mut map = Map::new();
325            for (key, item) in values {
326                let item_location = location_from_position(
327                    source,
328                    resource,
329                    single_line,
330                    &item.start,
331                    Some(&item.end),
332                );
333                let converted = convert_value(
334                    source,
335                    resource,
336                    text,
337                    single_line,
338                    item.value.clone(),
339                    &item.start,
340                    item_location,
341                )?;
342                map.insert(key, converted);
343            }
344            Ok(LocatedValue {
345                value: Value::Map(map),
346                location,
347            })
348        }
349    }
350}
351
352fn location_from_position(
353    source: &str,
354    resource: &str,
355    single_line: bool,
356    start: &Position,
357    end: Option<&Position>,
358) -> Location {
359    if single_line {
360        return Location::at(source, resource, None, None, None);
361    }
362    let mut length = None;
363    if let Some(end) = end
364        && start.line == end.line
365        && end.col >= start.col
366    {
367        length = Some(end.col - start.col + 1);
368    }
369    Location::at(source, resource, Some(start.line), Some(start.col), length)
370}
371
372#[cfg(all(test, feature = "json"))]
373mod tests {
374    use super::*;
375    use tanzim_source::SourceBuilder;
376
377    fn file_source(resource: &str) -> Source {
378        SourceBuilder::new()
379            .with_source("file")
380            .with_resource(resource)
381            .build()
382            .unwrap()
383    }
384
385    fn loc(value: Value) -> LocatedValue {
386        LocatedValue {
387            value,
388            location: Location::at("file", "test", None, None, None),
389        }
390    }
391
392    #[test]
393    fn unparses_complex_json() {
394        let mut nested = Map::new();
395        nested.insert("key".into(), loc(Value::String("va\"lue".into())));
396        let mut map = Map::new();
397        map.insert("name".into(), loc(Value::String("tanzim".into())));
398        map.insert("port".into(), loc(Value::Int(8080)));
399        map.insert("ratio".into(), loc(Value::Float(0.5)));
400        map.insert("debug".into(), loc(Value::Bool(true)));
401        map.insert(
402            "tags".into(),
403            loc(Value::List(vec![
404                loc(Value::String("a".into())),
405                loc(Value::String("b".into())),
406            ])),
407        );
408        map.insert("nested".into(), loc(Value::Map(nested)));
409
410        let text = unparse(&file_source("out.json"), Value::Map(map)).unwrap();
411        assert_eq!(
412            text,
413            "{\n  \"name\": \"tanzim\",\n  \"port\": 8080,\n  \"ratio\": 0.5,\n  \"debug\": true,\n  \"tags\": [\n    \"a\",\n    \"b\"\n  ],\n  \"nested\": {\n    \"key\": \"va\\\"lue\"\n  }\n}"
414        );
415    }
416
417    #[test]
418    fn parses_json_object() {
419        let parsed = Json::new()
420            .parse(&file_source("config.json"), br#"{"hello":"world"}"#)
421            .unwrap();
422        assert_eq!(
423            parsed
424                .value
425                .as_map()
426                .unwrap()
427                .get("hello")
428                .unwrap()
429                .value
430                .as_string()
431                .unwrap(),
432            "world"
433        );
434    }
435
436    #[test]
437    fn detects_json_format() {
438        let parser = Json::new();
439        assert_eq!(parser.is_format_supported(br#"{"a":1}"#), Some(true));
440        assert_eq!(parser.is_format_supported(b"not json"), Some(false));
441    }
442
443    #[test]
444    fn single_line_json_omits_position() {
445        let root = Json::new()
446            .parse(&file_source("a.json"), br#"{"a":1}"#)
447            .unwrap();
448        let map = root.value.as_map().unwrap();
449        let entry = map.get("a").unwrap();
450        assert_eq!(entry.location.line, None);
451        assert_eq!(entry.location.column, None);
452    }
453
454    #[test]
455    fn rejects_null() {
456        let error = Json::new()
457            .parse(&file_source("a.json"), b"{\n  \"a\": null\n}")
458            .unwrap_err();
459        assert!(matches!(error, Error::UnsupportedNull { .. }));
460        let message = format!("{error:#}");
461        assert!(message.contains('^'));
462        assert!(message.contains("null"));
463    }
464
465    #[test]
466    fn syntax_error_has_location() {
467        let error = Json::new()
468            .parse(&file_source("a.json"), b"{\n  \"a\":\n}\n")
469            .unwrap_err();
470        if let Error::Parse { ref location, .. } = error {
471            let location = location.as_ref().expect("syntax error location");
472            assert!(location.line.is_some());
473            assert!(location.column.is_some());
474        } else {
475            panic!("expected parse error");
476        }
477        let message = format!("{error:#}");
478        assert!(message.contains('^'));
479    }
480}