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