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