Skip to main content

tanzim_parse/
json.rs

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