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//!
23//! let value = Json::new()
24//!     .parse("file", "config.json", br#"{"host":"127.0.0.1"}"#)
25//!     .unwrap();
26//! assert_eq!(
27//!     value.value.as_map().unwrap().get("host").unwrap().value.as_string().unwrap(),
28//!     "127.0.0.1"
29//! );
30//! ```
31
32use crate::Parse;
33use crate::span::is_single_line;
34use cfg_if::cfg_if;
35use spanned_json_parser::value::Value as JsonValue;
36use spanned_json_parser::{Position, parse};
37use tanzim_value::{Error, LocatedValue, Location, Map, Value};
38
39/// Parser for the `json` format: standard JSON into a source-located value tree.
40///
41/// Objects, arrays, and scalars map to the value tree with a per-node span [`Location`]; JSON
42/// `null` is rejected with [`Error::UnsupportedNull`]. Stateless — construct with [`Json::new`].
43///
44/// ```
45/// use tanzim_parse::{Parse, json::Json};
46///
47/// let value = Json::new().parse("file", "config.json", br#"{"port":8080}"#).unwrap();
48/// let port = value.value.as_map().unwrap().get("port").unwrap();
49/// assert_eq!(port.value.as_int().unwrap(), 8080);
50/// ```
51#[derive(Clone, Copy, Default)]
52pub struct Json;
53
54impl Json {
55    /// Create a JSON parser.
56    pub fn new() -> Self {
57        Self
58    }
59}
60
61impl Parse for Json {
62    fn name(&self) -> &str {
63        "JSON"
64    }
65
66    fn supported_format_list(&self) -> Vec<String> {
67        vec!["json".into()]
68    }
69
70    fn parse(&self, source: &str, resource: &str, bytes: &[u8]) -> Result<LocatedValue, Error> {
71        cfg_if! {
72            if #[cfg(feature = "tracing")] {
73                tracing::debug!(msg = "Parsing JSON configuration", source = source, resource = resource, bytes = bytes.len());
74            } else if #[cfg(feature = "logging")] {
75                log::debug!("msg=\"Parsing JSON configuration\" source={source} resource={resource} bytes={}", bytes.len());
76            }
77        }
78        let text = match std::str::from_utf8(bytes) {
79            Ok(value) => value,
80            Err(_) => {
81                return Err(Error::InvalidUtf8 {
82                    location: Location::at(source, resource, None, None, None),
83                });
84            }
85        };
86        let single_line = is_single_line(bytes);
87        let parsed = match parse(text) {
88            Ok(value) => value,
89            Err(error) => {
90                return Err(Error::Parse {
91                    text: text.to_string(),
92                    location: Some(location_from_position(
93                        source,
94                        resource,
95                        single_line,
96                        &error.start,
97                        Some(&error.end),
98                    )),
99                    message: format!("{:?}", error.kind),
100                });
101            }
102        };
103        let location = location_from_position(
104            source,
105            resource,
106            single_line,
107            &parsed.start,
108            Some(&parsed.end),
109        );
110        let result = convert_value(
111            source,
112            resource,
113            text,
114            single_line,
115            parsed.value,
116            &parsed.start,
117            location,
118        );
119        if result.is_ok() {
120            cfg_if! {
121                if #[cfg(feature = "tracing")] {
122                    tracing::trace!(msg = "Parsed JSON configuration", source = source, resource = resource);
123                } else if #[cfg(feature = "logging")] {
124                    log::trace!("msg=\"Parsed JSON configuration\" source={source} resource={resource}");
125                }
126            }
127        }
128        result
129    }
130
131    fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
132        match std::str::from_utf8(bytes) {
133            Ok(text) => Some(parse(text).is_ok()),
134            Err(_) => Some(false),
135        }
136    }
137}
138
139fn convert_value(
140    source: &str,
141    resource: &str,
142    text: &str,
143    single_line: bool,
144    value: JsonValue,
145    _start: &Position,
146    location: Location,
147) -> Result<LocatedValue, Error> {
148    match value {
149        JsonValue::Null => Err(Error::UnsupportedNull {
150            text: text.to_string(),
151            location,
152        }),
153        JsonValue::Bool(value) => Ok(LocatedValue {
154            value: Value::Bool(value),
155            location,
156        }),
157        JsonValue::Number(number) => match number {
158            spanned_json_parser::value::Number::PosInt(value) => Ok(LocatedValue {
159                value: Value::Int(value as isize),
160                location,
161            }),
162            spanned_json_parser::value::Number::NegInt(value) => Ok(LocatedValue {
163                value: Value::Int(value as isize),
164                location,
165            }),
166            spanned_json_parser::value::Number::Float(value) => Ok(LocatedValue {
167                value: Value::Float(value),
168                location,
169            }),
170        },
171        JsonValue::String(value) => Ok(LocatedValue {
172            value: Value::String(value),
173            location,
174        }),
175        JsonValue::Array(values) => {
176            let mut list = Vec::new();
177            for item in &values {
178                let item_location = location_from_position(
179                    source,
180                    resource,
181                    single_line,
182                    &item.start,
183                    Some(&item.end),
184                );
185                let converted = convert_value(
186                    source,
187                    resource,
188                    text,
189                    single_line,
190                    item.value.clone(),
191                    &item.start,
192                    item_location,
193                )?;
194                list.push(converted);
195            }
196            Ok(LocatedValue {
197                value: Value::List(list),
198                location,
199            })
200        }
201        JsonValue::Object(values) => {
202            let mut map = Map::new();
203            for (key, item) in values {
204                let item_location = location_from_position(
205                    source,
206                    resource,
207                    single_line,
208                    &item.start,
209                    Some(&item.end),
210                );
211                let converted = convert_value(
212                    source,
213                    resource,
214                    text,
215                    single_line,
216                    item.value.clone(),
217                    &item.start,
218                    item_location,
219                )?;
220                map.insert(key, converted);
221            }
222            Ok(LocatedValue {
223                value: Value::Map(map),
224                location,
225            })
226        }
227    }
228}
229
230fn location_from_position(
231    source: &str,
232    resource: &str,
233    single_line: bool,
234    start: &Position,
235    end: Option<&Position>,
236) -> Location {
237    if single_line {
238        return Location::at(source, resource, None, None, None);
239    }
240    let mut length = None;
241    if let Some(end) = end
242        && start.line == end.line
243        && end.col >= start.col
244    {
245        length = Some(end.col - start.col + 1);
246    }
247    Location::at(source, resource, Some(start.line), Some(start.col), length)
248}
249
250#[cfg(all(test, feature = "json"))]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn parses_json_object() {
256        let parsed = Json::new()
257            .parse("file", "config.json", br#"{"hello":"world"}"#)
258            .unwrap();
259        assert_eq!(
260            parsed
261                .value
262                .as_map()
263                .unwrap()
264                .get("hello")
265                .unwrap()
266                .value
267                .as_string()
268                .unwrap(),
269            "world"
270        );
271    }
272
273    #[test]
274    fn detects_json_format() {
275        let parser = Json::new();
276        assert_eq!(parser.is_format_supported(br#"{"a":1}"#), Some(true));
277        assert_eq!(parser.is_format_supported(b"not json"), Some(false));
278    }
279
280    #[test]
281    fn single_line_json_omits_position() {
282        let root = Json::new().parse("file", "a.json", br#"{"a":1}"#).unwrap();
283        let map = root.value.as_map().unwrap();
284        let entry = map.get("a").unwrap();
285        assert_eq!(entry.location.line, None);
286        assert_eq!(entry.location.column, None);
287    }
288
289    #[test]
290    fn rejects_null() {
291        let error = Json::new()
292            .parse("file", "a.json", b"{\n  \"a\": null\n}")
293            .unwrap_err();
294        assert!(matches!(error, Error::UnsupportedNull { .. }));
295        let message = format!("{error:#}");
296        assert!(message.contains('^'));
297        assert!(message.contains("null"));
298    }
299
300    #[test]
301    fn syntax_error_has_location() {
302        let error = Json::new()
303            .parse("file", "a.json", b"{\n  \"a\":\n}\n")
304            .unwrap_err();
305        if let Error::Parse { ref location, .. } = error {
306            let location = location.as_ref().expect("syntax error location");
307            assert!(location.line.is_some());
308            assert!(location.column.is_some());
309        } else {
310            panic!("expected parse error");
311        }
312        let message = format!("{error:#}");
313        assert!(message.contains('^'));
314    }
315}