1use crate::span::is_single_line;
38use crate::{Parse, Source};
39use cfg_if::cfg_if;
40use spanned_json_parser::value::Value as JsonValue;
41use spanned_json_parser::{Position, parse};
42use tanzim_value::{Error, LocatedValue, Location, Map, Value};
43
44#[derive(Clone, Copy, Default)]
63pub struct Json;
64
65impl Json {
66 pub fn new() -> Self {
68 Self
69 }
70}
71
72impl Parse for Json {
73 fn name(&self) -> &str {
74 "JSON"
75 }
76
77 fn supported_format_list(&self) -> Vec<String> {
78 vec!["json".into()]
79 }
80
81 fn parse(&self, src: &Source, bytes: &[u8]) -> Result<LocatedValue, Error> {
82 #[cfg(any(feature = "tracing", feature = "logging"))]
83 let source = src.source();
84 #[cfg(any(feature = "tracing", feature = "logging"))]
85 let resource = src.resource();
86 cfg_if! {
87 if #[cfg(feature = "tracing")] {
88 tracing::debug!(msg = "Parsing JSON configuration", source = source, resource = resource, bytes = bytes.len());
89 } else if #[cfg(feature = "logging")] {
90 log::debug!("msg=\"Parsing JSON configuration\" source={source} resource={resource} bytes={}", bytes.len());
91 }
92 }
93 let text = match std::str::from_utf8(bytes) {
94 Ok(value) => value,
95 Err(_) => {
96 return Err(Error::InvalidUtf8 {
97 location: Box::new(Location::in_source(src.clone(), None, None, None)),
98 });
99 }
100 };
101 let single_line = is_single_line(bytes);
102 let parsed = match parse(text) {
103 Ok(value) => value,
104 Err(error) => {
105 return Err(Error::Parse {
106 text: text.to_string(),
107 location: Some(Box::new(location_from_position(
108 src,
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(src, single_line, &parsed.start, Some(&parsed.end));
118 let result = convert_value(
119 src,
120 text,
121 single_line,
122 parsed.value,
123 &parsed.start,
124 location,
125 );
126 if result.is_ok() {
127 cfg_if! {
128 if #[cfg(feature = "tracing")] {
129 tracing::trace!(msg = "Parsed JSON configuration", source = source, resource = resource);
130 } else if #[cfg(feature = "logging")] {
131 log::trace!("msg=\"Parsed JSON configuration\" source={source} resource={resource}");
132 }
133 }
134 }
135 result
136 }
137
138 fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
139 match std::str::from_utf8(bytes) {
140 Ok(text) => Some(parse(text).is_ok()),
141 Err(_) => Some(false),
142 }
143 }
144}
145
146pub fn unparse<V: AsRef<Value>>(
166 _source: &Source,
167 value: V,
168) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
169 let mut out = String::new();
170 write_json(&mut out, value.as_ref(), 0)?;
171 Ok(out)
172}
173
174fn write_json(
175 out: &mut String,
176 value: &Value,
177 indent: usize,
178) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
179 match value {
180 Value::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
181 Value::Int(value) => out.push_str(&value.to_string()),
182 Value::Float(value) => {
183 if !value.is_finite() {
184 return Err(format!("cannot serialize non-finite float {value} as JSON").into());
185 }
186 out.push_str(&format!("{value:?}"));
187 }
188 Value::String(value) => write_json_string(out, value),
189 Value::List(values) => {
190 if values.is_empty() {
191 out.push_str("[]");
192 return Ok(());
193 }
194 out.push_str("[\n");
195 for (index, item) in values.iter().enumerate() {
196 push_indent(out, indent + 1);
197 write_json(out, &item.value, indent + 1)?;
198 if index + 1 < values.len() {
199 out.push(',');
200 }
201 out.push('\n');
202 }
203 push_indent(out, indent);
204 out.push(']');
205 }
206 Value::Map(map) => {
207 let entries = map.entries();
208 if entries.is_empty() {
209 out.push_str("{}");
210 return Ok(());
211 }
212 out.push_str("{\n");
213 for (index, (key, item)) in entries.iter().enumerate() {
214 push_indent(out, indent + 1);
215 write_json_string(out, key);
216 out.push_str(": ");
217 write_json(out, &item.value, indent + 1)?;
218 if index + 1 < entries.len() {
219 out.push(',');
220 }
221 out.push('\n');
222 }
223 push_indent(out, indent);
224 out.push('}');
225 }
226 Value::Null => out.push_str("null"),
227 Value::Comment(_) => {
228 return Err("cannot serialize comment as JSON".into());
229 }
230 }
231 Ok(())
232}
233
234fn push_indent(out: &mut String, indent: usize) {
235 for _ in 0..indent {
236 out.push_str(" ");
237 }
238}
239
240fn write_json_string(out: &mut String, value: &str) {
241 out.push('"');
242 for ch in value.chars() {
243 match ch {
244 '"' => out.push_str("\\\""),
245 '\\' => out.push_str("\\\\"),
246 '\n' => out.push_str("\\n"),
247 '\r' => out.push_str("\\r"),
248 '\t' => out.push_str("\\t"),
249 control if (control as u32) < 0x20 => {
250 out.push_str(&format!("\\u{:04x}", control as u32));
251 }
252 other => out.push(other),
253 }
254 }
255 out.push('"');
256}
257
258fn convert_value(
259 source: &Source,
260 _text: &str,
261 single_line: bool,
262 value: JsonValue,
263 _start: &Position,
264 location: Location,
265) -> Result<LocatedValue, Error> {
266 match value {
267 JsonValue::Null => Ok(LocatedValue {
268 value: Value::Null,
269 location,
270 }),
271 JsonValue::Bool(value) => Ok(LocatedValue {
272 value: Value::Bool(value),
273 location,
274 }),
275 JsonValue::Number(number) => match number {
276 spanned_json_parser::value::Number::PosInt(value) => Ok(LocatedValue {
277 value: Value::Int(value as isize),
278 location,
279 }),
280 spanned_json_parser::value::Number::NegInt(value) => Ok(LocatedValue {
281 value: Value::Int(value as isize),
282 location,
283 }),
284 spanned_json_parser::value::Number::Float(value) => Ok(LocatedValue {
285 value: Value::Float(value),
286 location,
287 }),
288 },
289 JsonValue::String(value) => Ok(LocatedValue {
290 value: Value::String(value),
291 location,
292 }),
293 JsonValue::Array(values) => {
294 let mut list = Vec::new();
295 for item in &values {
296 let item_location =
297 location_from_position(source, single_line, &item.start, Some(&item.end));
298 let converted = convert_value(
299 source,
300 _text,
301 single_line,
302 item.value.clone(),
303 &item.start,
304 item_location,
305 )?;
306 list.push(converted);
307 }
308 Ok(LocatedValue {
309 value: Value::List(list),
310 location,
311 })
312 }
313 JsonValue::Object(values) => {
314 let mut map = Map::new();
315 for (key, item) in values {
316 let item_location =
317 location_from_position(source, single_line, &item.start, Some(&item.end));
318 let converted = convert_value(
319 source,
320 _text,
321 single_line,
322 item.value.clone(),
323 &item.start,
324 item_location,
325 )?;
326 map.insert(key, converted);
327 }
328 Ok(LocatedValue {
329 value: Value::Map(map),
330 location,
331 })
332 }
333 }
334}
335
336fn location_from_position(
337 source: &Source,
338 single_line: bool,
339 start: &Position,
340 end: Option<&Position>,
341) -> Location {
342 if single_line {
343 return Location::in_source(source.clone(), None, None, None);
344 }
345 let mut length = None;
346 if let Some(end) = end
347 && start.line == end.line
348 && end.col >= start.col
349 {
350 length = Some(end.col - start.col + 1);
351 }
352 Location::in_source(source.clone(), Some(start.line), Some(start.col), length)
353}
354
355#[cfg(all(test, feature = "json"))]
356mod tests {
357 use super::*;
358 use tanzim_source::SourceBuilder;
359
360 fn file_source(resource: &str) -> Source {
361 SourceBuilder::new()
362 .with_source("file")
363 .with_resource(resource)
364 .build()
365 .unwrap()
366 }
367
368 fn loc(value: Value) -> LocatedValue {
369 LocatedValue {
370 value,
371 location: Location::at("file", "test", None, None, None),
372 }
373 }
374
375 #[test]
376 fn unparses_complex_json() {
377 let mut nested = Map::new();
378 nested.insert("key".into(), loc(Value::String("va\"lue".into())));
379 let mut map = Map::new();
380 map.insert("name".into(), loc(Value::String("tanzim".into())));
381 map.insert("port".into(), loc(Value::Int(8080)));
382 map.insert("ratio".into(), loc(Value::Float(0.5)));
383 map.insert("debug".into(), loc(Value::Bool(true)));
384 map.insert(
385 "tags".into(),
386 loc(Value::List(vec![
387 loc(Value::String("a".into())),
388 loc(Value::String("b".into())),
389 ])),
390 );
391 map.insert("nested".into(), loc(Value::Map(nested)));
392
393 let text = unparse(&file_source("out.json"), Value::Map(map)).unwrap();
394 assert_eq!(
395 text,
396 "{\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}"
397 );
398 }
399
400 #[test]
401 fn parses_json_object() {
402 let parsed = Json::new()
403 .parse(&file_source("config.json"), br#"{"hello":"world"}"#)
404 .unwrap();
405 assert_eq!(
406 parsed
407 .value
408 .as_map()
409 .unwrap()
410 .get("hello")
411 .unwrap()
412 .value
413 .as_string()
414 .unwrap(),
415 "world"
416 );
417 }
418
419 #[test]
420 fn detects_json_format() {
421 let parser = Json::new();
422 assert_eq!(parser.is_format_supported(br#"{"a":1}"#), Some(true));
423 assert_eq!(parser.is_format_supported(b"not json"), Some(false));
424 }
425
426 #[test]
427 fn single_line_json_omits_position() {
428 let root = Json::new()
429 .parse(&file_source("a.json"), br#"{"a":1}"#)
430 .unwrap();
431 let map = root.value.as_map().unwrap();
432 let entry = map.get("a").unwrap();
433 assert_eq!(entry.location.line, None);
434 assert_eq!(entry.location.column, None);
435 }
436
437 #[test]
438 fn parses_null() {
439 let root = Json::new()
440 .parse(&file_source("a.json"), b"{\n \"a\": null\n}")
441 .unwrap();
442 let map = root.value.as_map().unwrap();
443 let entry = map.get("a").unwrap();
444 assert!(entry.value.is_null());
445 assert_eq!(entry.location.line, std::num::NonZeroU32::new(2));
446 }
447
448 #[test]
449 fn syntax_error_has_location() {
450 let error = Json::new()
451 .parse(&file_source("a.json"), b"{\n \"a\":\n}\n")
452 .unwrap_err();
453 if let Error::Parse { ref location, .. } = error {
454 let location = location.as_ref().expect("syntax error location");
455 assert!(location.line.is_some());
456 assert!(location.column.is_some());
457 } else {
458 panic!("expected parse error");
459 }
460 let message = format!("{error:#}");
461 assert!(message.contains('^'));
462 }
463}