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
153fn convert_value(
154    source: &str,
155    resource: &str,
156    text: &str,
157    single_line: bool,
158    value: JsonValue,
159    _start: &Position,
160    location: Location,
161) -> Result<LocatedValue, Error> {
162    match value {
163        JsonValue::Null => Err(Error::UnsupportedNull {
164            text: text.to_string(),
165            location,
166        }),
167        JsonValue::Bool(value) => Ok(LocatedValue {
168            value: Value::Bool(value),
169            location,
170        }),
171        JsonValue::Number(number) => match number {
172            spanned_json_parser::value::Number::PosInt(value) => Ok(LocatedValue {
173                value: Value::Int(value as isize),
174                location,
175            }),
176            spanned_json_parser::value::Number::NegInt(value) => Ok(LocatedValue {
177                value: Value::Int(value as isize),
178                location,
179            }),
180            spanned_json_parser::value::Number::Float(value) => Ok(LocatedValue {
181                value: Value::Float(value),
182                location,
183            }),
184        },
185        JsonValue::String(value) => Ok(LocatedValue {
186            value: Value::String(value),
187            location,
188        }),
189        JsonValue::Array(values) => {
190            let mut list = Vec::new();
191            for item in &values {
192                let item_location = location_from_position(
193                    source,
194                    resource,
195                    single_line,
196                    &item.start,
197                    Some(&item.end),
198                );
199                let converted = convert_value(
200                    source,
201                    resource,
202                    text,
203                    single_line,
204                    item.value.clone(),
205                    &item.start,
206                    item_location,
207                )?;
208                list.push(converted);
209            }
210            Ok(LocatedValue {
211                value: Value::List(list),
212                location,
213            })
214        }
215        JsonValue::Object(values) => {
216            let mut map = Map::new();
217            for (key, item) in values {
218                let item_location = location_from_position(
219                    source,
220                    resource,
221                    single_line,
222                    &item.start,
223                    Some(&item.end),
224                );
225                let converted = convert_value(
226                    source,
227                    resource,
228                    text,
229                    single_line,
230                    item.value.clone(),
231                    &item.start,
232                    item_location,
233                )?;
234                map.insert(key, converted);
235            }
236            Ok(LocatedValue {
237                value: Value::Map(map),
238                location,
239            })
240        }
241    }
242}
243
244fn location_from_position(
245    source: &str,
246    resource: &str,
247    single_line: bool,
248    start: &Position,
249    end: Option<&Position>,
250) -> Location {
251    if single_line {
252        return Location::at(source, resource, None, None, None);
253    }
254    let mut length = None;
255    if let Some(end) = end
256        && start.line == end.line
257        && end.col >= start.col
258    {
259        length = Some(end.col - start.col + 1);
260    }
261    Location::at(source, resource, Some(start.line), Some(start.col), length)
262}
263
264#[cfg(all(test, feature = "json"))]
265mod tests {
266    use super::*;
267    use tanzim_source::SourceBuilder;
268
269    fn file_source(resource: &str) -> Source {
270        SourceBuilder::new()
271            .with_source("file")
272            .with_resource(resource)
273            .build()
274            .unwrap()
275    }
276
277    #[test]
278    fn parses_json_object() {
279        let parsed = Json::new()
280            .parse(&file_source("config.json"), br#"{"hello":"world"}"#)
281            .unwrap();
282        assert_eq!(
283            parsed
284                .value
285                .as_map()
286                .unwrap()
287                .get("hello")
288                .unwrap()
289                .value
290                .as_string()
291                .unwrap(),
292            "world"
293        );
294    }
295
296    #[test]
297    fn detects_json_format() {
298        let parser = Json::new();
299        assert_eq!(parser.is_format_supported(br#"{"a":1}"#), Some(true));
300        assert_eq!(parser.is_format_supported(b"not json"), Some(false));
301    }
302
303    #[test]
304    fn single_line_json_omits_position() {
305        let root = Json::new()
306            .parse(&file_source("a.json"), br#"{"a":1}"#)
307            .unwrap();
308        let map = root.value.as_map().unwrap();
309        let entry = map.get("a").unwrap();
310        assert_eq!(entry.location.line, None);
311        assert_eq!(entry.location.column, None);
312    }
313
314    #[test]
315    fn rejects_null() {
316        let error = Json::new()
317            .parse(&file_source("a.json"), b"{\n  \"a\": null\n}")
318            .unwrap_err();
319        assert!(matches!(error, Error::UnsupportedNull { .. }));
320        let message = format!("{error:#}");
321        assert!(message.contains('^'));
322        assert!(message.contains("null"));
323    }
324
325    #[test]
326    fn syntax_error_has_location() {
327        let error = Json::new()
328            .parse(&file_source("a.json"), b"{\n  \"a\":\n}\n")
329            .unwrap_err();
330        if let Error::Parse { ref location, .. } = error {
331            let location = location.as_ref().expect("syntax error location");
332            assert!(location.line.is_some());
333            assert!(location.column.is_some());
334        } else {
335            panic!("expected parse error");
336        }
337        let message = format!("{error:#}");
338        assert!(message.contains('^'));
339    }
340}