1use 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#[derive(Clone, Copy, Default)]
52pub struct Json;
53
54impl Json {
55 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}