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