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
153pub fn unparse<V: AsRef<Value>>(
173 _source: &Source,
174 value: V,
175) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
176 let mut out = String::new();
177 write_json(&mut out, value.as_ref(), 0)?;
178 Ok(out)
179}
180
181fn write_json(
182 out: &mut String,
183 value: &Value,
184 indent: usize,
185) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
186 match value {
187 Value::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
188 Value::Int(value) => out.push_str(&value.to_string()),
189 Value::Float(value) => {
190 if !value.is_finite() {
191 return Err(format!("cannot serialize non-finite float {value} as JSON").into());
192 }
193 out.push_str(&format!("{value:?}"));
194 }
195 Value::String(value) => write_json_string(out, value),
196 Value::List(values) => {
197 if values.is_empty() {
198 out.push_str("[]");
199 return Ok(());
200 }
201 out.push_str("[\n");
202 for (index, item) in values.iter().enumerate() {
203 push_indent(out, indent + 1);
204 write_json(out, &item.value, indent + 1)?;
205 if index + 1 < values.len() {
206 out.push(',');
207 }
208 out.push('\n');
209 }
210 push_indent(out, indent);
211 out.push(']');
212 }
213 Value::Map(map) => {
214 let entries = map.entries();
215 if entries.is_empty() {
216 out.push_str("{}");
217 return Ok(());
218 }
219 out.push_str("{\n");
220 for (index, (key, item)) in entries.iter().enumerate() {
221 push_indent(out, indent + 1);
222 write_json_string(out, key);
223 out.push_str(": ");
224 write_json(out, &item.value, indent + 1)?;
225 if index + 1 < entries.len() {
226 out.push(',');
227 }
228 out.push('\n');
229 }
230 push_indent(out, indent);
231 out.push('}');
232 }
233 }
234 Ok(())
235}
236
237fn push_indent(out: &mut String, indent: usize) {
238 for _ in 0..indent {
239 out.push_str(" ");
240 }
241}
242
243fn write_json_string(out: &mut String, value: &str) {
244 out.push('"');
245 for ch in value.chars() {
246 match ch {
247 '"' => out.push_str("\\\""),
248 '\\' => out.push_str("\\\\"),
249 '\n' => out.push_str("\\n"),
250 '\r' => out.push_str("\\r"),
251 '\t' => out.push_str("\\t"),
252 control if (control as u32) < 0x20 => {
253 out.push_str(&format!("\\u{:04x}", control as u32));
254 }
255 other => out.push(other),
256 }
257 }
258 out.push('"');
259}
260
261fn convert_value(
262 source: &str,
263 resource: &str,
264 text: &str,
265 single_line: bool,
266 value: JsonValue,
267 _start: &Position,
268 location: Location,
269) -> Result<LocatedValue, Error> {
270 match value {
271 JsonValue::Null => Err(Error::UnsupportedNull {
272 text: text.to_string(),
273 location,
274 }),
275 JsonValue::Bool(value) => Ok(LocatedValue {
276 value: Value::Bool(value),
277 location,
278 }),
279 JsonValue::Number(number) => match number {
280 spanned_json_parser::value::Number::PosInt(value) => Ok(LocatedValue {
281 value: Value::Int(value as isize),
282 location,
283 }),
284 spanned_json_parser::value::Number::NegInt(value) => Ok(LocatedValue {
285 value: Value::Int(value as isize),
286 location,
287 }),
288 spanned_json_parser::value::Number::Float(value) => Ok(LocatedValue {
289 value: Value::Float(value),
290 location,
291 }),
292 },
293 JsonValue::String(value) => Ok(LocatedValue {
294 value: Value::String(value),
295 location,
296 }),
297 JsonValue::Array(values) => {
298 let mut list = Vec::new();
299 for item in &values {
300 let item_location = location_from_position(
301 source,
302 resource,
303 single_line,
304 &item.start,
305 Some(&item.end),
306 );
307 let converted = convert_value(
308 source,
309 resource,
310 text,
311 single_line,
312 item.value.clone(),
313 &item.start,
314 item_location,
315 )?;
316 list.push(converted);
317 }
318 Ok(LocatedValue {
319 value: Value::List(list),
320 location,
321 })
322 }
323 JsonValue::Object(values) => {
324 let mut map = Map::new();
325 for (key, item) in values {
326 let item_location = location_from_position(
327 source,
328 resource,
329 single_line,
330 &item.start,
331 Some(&item.end),
332 );
333 let converted = convert_value(
334 source,
335 resource,
336 text,
337 single_line,
338 item.value.clone(),
339 &item.start,
340 item_location,
341 )?;
342 map.insert(key, converted);
343 }
344 Ok(LocatedValue {
345 value: Value::Map(map),
346 location,
347 })
348 }
349 }
350}
351
352fn location_from_position(
353 source: &str,
354 resource: &str,
355 single_line: bool,
356 start: &Position,
357 end: Option<&Position>,
358) -> Location {
359 if single_line {
360 return Location::at(source, resource, None, None, None);
361 }
362 let mut length = None;
363 if let Some(end) = end
364 && start.line == end.line
365 && end.col >= start.col
366 {
367 length = Some(end.col - start.col + 1);
368 }
369 Location::at(source, resource, Some(start.line), Some(start.col), length)
370}
371
372#[cfg(all(test, feature = "json"))]
373mod tests {
374 use super::*;
375 use tanzim_source::SourceBuilder;
376
377 fn file_source(resource: &str) -> Source {
378 SourceBuilder::new()
379 .with_source("file")
380 .with_resource(resource)
381 .build()
382 .unwrap()
383 }
384
385 fn loc(value: Value) -> LocatedValue {
386 LocatedValue {
387 value,
388 location: Location::at("file", "test", None, None, None),
389 }
390 }
391
392 #[test]
393 fn unparses_complex_json() {
394 let mut nested = Map::new();
395 nested.insert("key".into(), loc(Value::String("va\"lue".into())));
396 let mut map = Map::new();
397 map.insert("name".into(), loc(Value::String("tanzim".into())));
398 map.insert("port".into(), loc(Value::Int(8080)));
399 map.insert("ratio".into(), loc(Value::Float(0.5)));
400 map.insert("debug".into(), loc(Value::Bool(true)));
401 map.insert(
402 "tags".into(),
403 loc(Value::List(vec![
404 loc(Value::String("a".into())),
405 loc(Value::String("b".into())),
406 ])),
407 );
408 map.insert("nested".into(), loc(Value::Map(nested)));
409
410 let text = unparse(&file_source("out.json"), Value::Map(map)).unwrap();
411 assert_eq!(
412 text,
413 "{\n \"name\": \"tanzim\",\n \"port\": 8080,\n \"ratio\": 0.5,\n \"debug\": true,\n \"tags\": [\n \"a\",\n \"b\"\n ],\n \"nested\": {\n \"key\": \"va\\\"lue\"\n }\n}"
414 );
415 }
416
417 #[test]
418 fn parses_json_object() {
419 let parsed = Json::new()
420 .parse(&file_source("config.json"), br#"{"hello":"world"}"#)
421 .unwrap();
422 assert_eq!(
423 parsed
424 .value
425 .as_map()
426 .unwrap()
427 .get("hello")
428 .unwrap()
429 .value
430 .as_string()
431 .unwrap(),
432 "world"
433 );
434 }
435
436 #[test]
437 fn detects_json_format() {
438 let parser = Json::new();
439 assert_eq!(parser.is_format_supported(br#"{"a":1}"#), Some(true));
440 assert_eq!(parser.is_format_supported(b"not json"), Some(false));
441 }
442
443 #[test]
444 fn single_line_json_omits_position() {
445 let root = Json::new()
446 .parse(&file_source("a.json"), br#"{"a":1}"#)
447 .unwrap();
448 let map = root.value.as_map().unwrap();
449 let entry = map.get("a").unwrap();
450 assert_eq!(entry.location.line, None);
451 assert_eq!(entry.location.column, None);
452 }
453
454 #[test]
455 fn rejects_null() {
456 let error = Json::new()
457 .parse(&file_source("a.json"), b"{\n \"a\": null\n}")
458 .unwrap_err();
459 assert!(matches!(error, Error::UnsupportedNull { .. }));
460 let message = format!("{error:#}");
461 assert!(message.contains('^'));
462 assert!(message.contains("null"));
463 }
464
465 #[test]
466 fn syntax_error_has_location() {
467 let error = Json::new()
468 .parse(&file_source("a.json"), b"{\n \"a\":\n}\n")
469 .unwrap_err();
470 if let Error::Parse { ref location, .. } = error {
471 let location = location.as_ref().expect("syntax error location");
472 assert!(location.line.is_some());
473 assert!(location.column.is_some());
474 } else {
475 panic!("expected parse error");
476 }
477 let message = format!("{error:#}");
478 assert!(message.contains('^'));
479 }
480}