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