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