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                    location: Some(Box::new(location_from_position(
112                        src,
113                        text,
114                        single_line,
115                        &error.start,
116                        Some(&error.end),
117                    ))),
118                    message: format!("{:?}", error.kind),
119                });
120            }
121        };
122        let location =
123            location_from_position(src, text, single_line, &parsed.start, Some(&parsed.end));
124        let result = convert_value(
125            src,
126            text,
127            single_line,
128            parsed.value,
129            &parsed.start,
130            location,
131        );
132        if result.is_ok() {
133            cfg_if! {
134                if #[cfg(feature = "tracing")] {
135                    tracing::trace!(msg = "Parsed JSON configuration", source = source, resource = resource);
136                } else if #[cfg(feature = "logging")] {
137                    log::trace!("msg=\"Parsed JSON configuration\" source={source} resource={resource}");
138                }
139            }
140        }
141        result
142    }
143
144    fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
145        match std::str::from_utf8(bytes) {
146            Ok(text) => Some(parse(text).is_ok()),
147            Err(_) => Some(false),
148        }
149    }
150}
151
152/// Serialize a [`Value`] tree into pretty-printed JSON (2-space indent).
153///
154/// Accepts a [`Value`], `&Value`, [`LocatedValue`], or `&LocatedValue`. `source` is
155/// accepted for signature symmetry with [`Parse::parse`] but is unused here.
156///
157/// ```
158/// use tanzim_parse::json::unparse;
159/// use tanzim_source::SourceBuilder;
160/// use tanzim_value::{Map, LocatedValue, Location, Value};
161///
162/// let source = SourceBuilder::new().with_source("file").build().unwrap();
163/// let mut map = Map::new();
164/// map.insert("port".into(), LocatedValue::new(
165///     Value::Int(8080),
166///     Location::at("file", "", None, None, None),
167/// ));
168/// let text = unparse(&source, Value::Map(map)).unwrap();
169/// assert_eq!(text, "{\n  \"port\": 8080\n}");
170/// ```
171pub fn unparse<V: AsRef<Value>>(
172    _source: &Source,
173    value: V,
174) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
175    let mut out = String::new();
176    write_json(&mut out, value.as_ref(), 0)?;
177    Ok(out)
178}
179
180fn write_json(
181    out: &mut String,
182    value: &Value,
183    indent: usize,
184) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
185    match value {
186        Value::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
187        Value::Int(value) => out.push_str(&value.to_string()),
188        Value::Float(value) => {
189            if !value.is_finite() {
190                return Err(format!("cannot serialize non-finite float {value} as JSON").into());
191            }
192            out.push_str(&format!("{value:?}"));
193        }
194        Value::String(value) => write_json_string(out, value),
195        Value::List(values) => {
196            if values.is_empty() {
197                out.push_str("[]");
198                return Ok(());
199            }
200            out.push_str("[\n");
201            for (index, item) in values.iter().enumerate() {
202                push_indent(out, indent + 1);
203                write_json(out, item.value(), indent + 1)?;
204                if index + 1 < values.len() {
205                    out.push(',');
206                }
207                out.push('\n');
208            }
209            push_indent(out, indent);
210            out.push(']');
211        }
212        Value::Map(map) => {
213            let entries = map.entries();
214            if entries.is_empty() {
215                out.push_str("{}");
216                return Ok(());
217            }
218            out.push_str("{\n");
219            for (index, (key, item)) in entries.iter().enumerate() {
220                push_indent(out, indent + 1);
221                write_json_string(out, key);
222                out.push_str(": ");
223                write_json(out, item.value(), indent + 1)?;
224                if index + 1 < entries.len() {
225                    out.push(',');
226                }
227                out.push('\n');
228            }
229            push_indent(out, indent);
230            out.push('}');
231        }
232        Value::Null => out.push_str("null"),
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: &Source,
263    text: &str,
264    single_line: bool,
265    value: JsonValue,
266    _start: &Position,
267    location: Location,
268) -> Result<LocatedValue, Error> {
269    match value {
270        JsonValue::Null => Ok(LocatedValue::new(Value::Null, location)),
271        JsonValue::Bool(value) => Ok(LocatedValue::new(Value::Bool(value), location)),
272        JsonValue::Number(number) => match number {
273            spanned_json_parser::value::Number::PosInt(value) => {
274                Ok(LocatedValue::new(Value::Int(value as isize), location))
275            }
276            spanned_json_parser::value::Number::NegInt(value) => {
277                Ok(LocatedValue::new(Value::Int(value as isize), location))
278            }
279            spanned_json_parser::value::Number::Float(value) => {
280                Ok(LocatedValue::new(Value::Float(value), location))
281            }
282        },
283        JsonValue::String(value) => Ok(LocatedValue::new(Value::String(value), location)),
284        JsonValue::Array(values) => {
285            let mut list = Vec::new();
286            for item in &values {
287                let item_location =
288                    location_from_position(source, text, single_line, &item.start, Some(&item.end));
289                let converted = convert_value(
290                    source,
291                    text,
292                    single_line,
293                    item.value.clone(),
294                    &item.start,
295                    item_location,
296                )?;
297                list.push(converted);
298            }
299            Ok(LocatedValue::new(Value::List(list), location))
300        }
301        JsonValue::Object(values) => {
302            let mut map = Map::new();
303            for (key, item) in values {
304                let item_location =
305                    location_from_position(source, text, single_line, &item.start, Some(&item.end));
306                let converted = convert_value(
307                    source,
308                    text,
309                    single_line,
310                    item.value.clone(),
311                    &item.start,
312                    item_location,
313                )?;
314                map.insert(key, converted);
315            }
316            Ok(LocatedValue::new(Value::Map(map), location))
317        }
318    }
319}
320
321fn location_from_position(
322    source: &Source,
323    text: &str,
324    single_line: bool,
325    start: &Position,
326    end: Option<&Position>,
327) -> Location {
328    if single_line {
329        return Location::in_source(source.clone(), None, None, None);
330    }
331    let mut length = None;
332    if let Some(end) = end
333        && start.line == end.line
334        && end.col >= start.col
335    {
336        length = Some(end.col - start.col + 1);
337    }
338    Location::in_text(
339        source.clone(),
340        text,
341        Some(start.line),
342        Some(start.col),
343        length,
344    )
345}