1use 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#[derive(Clone, Copy, Default)]
64pub struct Json;
65
66impl Json {
67 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}