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 {
159///     value: Value::Int(8080),
160///     location: 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        Value::Comment(_) => {
228            return Err("cannot serialize comment as JSON".into());
229        }
230    }
231    Ok(())
232}
233
234fn push_indent(out: &mut String, indent: usize) {
235    for _ in 0..indent {
236        out.push_str("  ");
237    }
238}
239
240fn write_json_string(out: &mut String, value: &str) {
241    out.push('"');
242    for ch in value.chars() {
243        match ch {
244            '"' => out.push_str("\\\""),
245            '\\' => out.push_str("\\\\"),
246            '\n' => out.push_str("\\n"),
247            '\r' => out.push_str("\\r"),
248            '\t' => out.push_str("\\t"),
249            control if (control as u32) < 0x20 => {
250                out.push_str(&format!("\\u{:04x}", control as u32));
251            }
252            other => out.push(other),
253        }
254    }
255    out.push('"');
256}
257
258fn convert_value(
259    source: &Source,
260    _text: &str,
261    single_line: bool,
262    value: JsonValue,
263    _start: &Position,
264    location: Location,
265) -> Result<LocatedValue, Error> {
266    match value {
267        JsonValue::Null => Ok(LocatedValue {
268            value: Value::Null,
269            location,
270        }),
271        JsonValue::Bool(value) => Ok(LocatedValue {
272            value: Value::Bool(value),
273            location,
274        }),
275        JsonValue::Number(number) => match number {
276            spanned_json_parser::value::Number::PosInt(value) => Ok(LocatedValue {
277                value: Value::Int(value as isize),
278                location,
279            }),
280            spanned_json_parser::value::Number::NegInt(value) => Ok(LocatedValue {
281                value: Value::Int(value as isize),
282                location,
283            }),
284            spanned_json_parser::value::Number::Float(value) => Ok(LocatedValue {
285                value: Value::Float(value),
286                location,
287            }),
288        },
289        JsonValue::String(value) => Ok(LocatedValue {
290            value: Value::String(value),
291            location,
292        }),
293        JsonValue::Array(values) => {
294            let mut list = Vec::new();
295            for item in &values {
296                let item_location =
297                    location_from_position(source, single_line, &item.start, Some(&item.end));
298                let converted = convert_value(
299                    source,
300                    _text,
301                    single_line,
302                    item.value.clone(),
303                    &item.start,
304                    item_location,
305                )?;
306                list.push(converted);
307            }
308            Ok(LocatedValue {
309                value: Value::List(list),
310                location,
311            })
312        }
313        JsonValue::Object(values) => {
314            let mut map = Map::new();
315            for (key, item) in values {
316                let item_location =
317                    location_from_position(source, single_line, &item.start, Some(&item.end));
318                let converted = convert_value(
319                    source,
320                    _text,
321                    single_line,
322                    item.value.clone(),
323                    &item.start,
324                    item_location,
325                )?;
326                map.insert(key, converted);
327            }
328            Ok(LocatedValue {
329                value: Value::Map(map),
330                location,
331            })
332        }
333    }
334}
335
336fn location_from_position(
337    source: &Source,
338    single_line: bool,
339    start: &Position,
340    end: Option<&Position>,
341) -> Location {
342    if single_line {
343        return Location::in_source(source.clone(), None, None, None);
344    }
345    let mut length = None;
346    if let Some(end) = end
347        && start.line == end.line
348        && end.col >= start.col
349    {
350        length = Some(end.col - start.col + 1);
351    }
352    Location::in_source(source.clone(), Some(start.line), Some(start.col), length)
353}
354
355#[cfg(all(test, feature = "json"))]
356mod tests {
357    use super::*;
358    use tanzim_source::SourceBuilder;
359
360    fn file_source(resource: &str) -> Source {
361        SourceBuilder::new()
362            .with_source("file")
363            .with_resource(resource)
364            .build()
365            .unwrap()
366    }
367
368    fn loc(value: Value) -> LocatedValue {
369        LocatedValue {
370            value,
371            location: Location::at("file", "test", None, None, None),
372        }
373    }
374
375    #[test]
376    fn unparses_complex_json() {
377        let mut nested = Map::new();
378        nested.insert("key".into(), loc(Value::String("va\"lue".into())));
379        let mut map = Map::new();
380        map.insert("name".into(), loc(Value::String("tanzim".into())));
381        map.insert("port".into(), loc(Value::Int(8080)));
382        map.insert("ratio".into(), loc(Value::Float(0.5)));
383        map.insert("debug".into(), loc(Value::Bool(true)));
384        map.insert(
385            "tags".into(),
386            loc(Value::List(vec![
387                loc(Value::String("a".into())),
388                loc(Value::String("b".into())),
389            ])),
390        );
391        map.insert("nested".into(), loc(Value::Map(nested)));
392
393        let text = unparse(&file_source("out.json"), Value::Map(map)).unwrap();
394        assert_eq!(
395            text,
396            "{\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}"
397        );
398    }
399
400    #[test]
401    fn parses_json_object() {
402        let parsed = Json::new()
403            .parse(&file_source("config.json"), br#"{"hello":"world"}"#)
404            .unwrap();
405        assert_eq!(
406            parsed
407                .value
408                .as_map()
409                .unwrap()
410                .get("hello")
411                .unwrap()
412                .value
413                .as_string()
414                .unwrap(),
415            "world"
416        );
417    }
418
419    #[test]
420    fn detects_json_format() {
421        let parser = Json::new();
422        assert_eq!(parser.is_format_supported(br#"{"a":1}"#), Some(true));
423        assert_eq!(parser.is_format_supported(b"not json"), Some(false));
424    }
425
426    #[test]
427    fn single_line_json_omits_position() {
428        let root = Json::new()
429            .parse(&file_source("a.json"), br#"{"a":1}"#)
430            .unwrap();
431        let map = root.value.as_map().unwrap();
432        let entry = map.get("a").unwrap();
433        assert_eq!(entry.location.line, None);
434        assert_eq!(entry.location.column, None);
435    }
436
437    #[test]
438    fn parses_null() {
439        let root = Json::new()
440            .parse(&file_source("a.json"), b"{\n  \"a\": null\n}")
441            .unwrap();
442        let map = root.value.as_map().unwrap();
443        let entry = map.get("a").unwrap();
444        assert!(entry.value.is_null());
445        assert_eq!(entry.location.line, std::num::NonZeroU32::new(2));
446    }
447
448    #[test]
449    fn syntax_error_has_location() {
450        let error = Json::new()
451            .parse(&file_source("a.json"), b"{\n  \"a\":\n}\n")
452            .unwrap_err();
453        if let Error::Parse { ref location, .. } = error {
454            let location = location.as_ref().expect("syntax error location");
455            assert!(location.line.is_some());
456            assert!(location.column.is_some());
457        } else {
458            panic!("expected parse error");
459        }
460        let message = format!("{error:#}");
461        assert!(message.contains('^'));
462    }
463}