1use hedl_core::lex::Tensor;
21use hedl_core::{Document, Item, MatrixList, Node, Value};
22use serde_json::{json, Map, Number, Value as JsonValue};
23use std::collections::BTreeMap;
24
25#[derive(Debug, Clone)]
27pub struct ToJsonConfig {
28 pub include_metadata: bool,
30 pub flatten_lists: bool,
32 pub include_children: bool,
34 pub ascii_safe: bool,
48}
49
50impl Default for ToJsonConfig {
51 fn default() -> Self {
52 Self {
53 include_metadata: false,
54 flatten_lists: false,
55 include_children: true, ascii_safe: false,
57 }
58 }
59}
60
61impl hedl_core::convert::ExportConfig for ToJsonConfig {
62 fn include_metadata(&self) -> bool {
63 self.include_metadata
64 }
65
66 fn pretty(&self) -> bool {
67 true
69 }
70}
71
72pub fn to_json(doc: &Document, config: &ToJsonConfig) -> Result<String, String> {
74 let value = to_json_value(doc, config)?;
75
76 if config.ascii_safe {
77 let json = serde_json::to_string_pretty(&value)
79 .map_err(|e| format!("JSON serialization error: {e}"))?;
80 Ok(escape_non_ascii(&json))
81 } else {
82 serde_json::to_string_pretty(&value).map_err(|e| format!("JSON serialization error: {e}"))
83 }
84}
85
86fn escape_non_ascii(json: &str) -> String {
91 let mut result = String::with_capacity(json.len());
92 let mut in_string = false;
93 let mut escape_next = false;
94
95 for ch in json.chars() {
96 if escape_next {
97 result.push(ch);
99 escape_next = false;
100 continue;
101 }
102
103 if ch == '\\' && in_string {
104 result.push(ch);
105 escape_next = true;
106 continue;
107 }
108
109 if ch == '"' {
110 in_string = !in_string;
111 result.push(ch);
112 continue;
113 }
114
115 if in_string && !ch.is_ascii() {
116 let code_point = ch as u32;
118
119 if code_point <= 0xFFFF {
120 result.push_str(&format!("\\u{code_point:04x}"));
122 } else {
123 let adjusted = code_point - 0x10000;
125 let high = 0xD800 | ((adjusted >> 10) & 0x3FF);
126 let low = 0xDC00 | (adjusted & 0x3FF);
127 result.push_str(&format!("\\u{high:04x}\\u{low:04x}"));
128 }
129 } else {
130 result.push(ch);
131 }
132 }
133
134 result
135}
136
137pub fn to_json_value(doc: &Document, config: &ToJsonConfig) -> Result<JsonValue, String> {
139 root_to_json(&doc.root, doc, config)
140}
141
142fn root_to_json(
143 root: &BTreeMap<String, Item>,
144 doc: &Document,
145 config: &ToJsonConfig,
146) -> Result<JsonValue, String> {
147 let mut map = Map::with_capacity(root.len());
149
150 for (key, item) in root {
151 let json_value = item_to_json(item, doc, config)?;
152 map.insert(key.clone(), json_value);
153 }
154
155 Ok(JsonValue::Object(map))
156}
157
158fn item_to_json(item: &Item, doc: &Document, config: &ToJsonConfig) -> Result<JsonValue, String> {
159 match item {
160 Item::Scalar(value) => Ok(value_to_json(value)),
161 Item::Object(obj) => object_to_json(obj, doc, config),
162 Item::List(list) => matrix_list_to_json(list, doc, config),
163 }
164}
165
166fn object_to_json(
167 obj: &BTreeMap<String, Item>,
168 doc: &Document,
169 config: &ToJsonConfig,
170) -> Result<JsonValue, String> {
171 let mut map = Map::with_capacity(obj.len());
173
174 for (key, item) in obj {
175 let json_value = item_to_json(item, doc, config)?;
176 map.insert(key.clone(), json_value);
177 }
178
179 Ok(JsonValue::Object(map))
180}
181
182fn value_to_json(value: &Value) -> JsonValue {
183 match value {
184 Value::Null => JsonValue::Null,
185 Value::Bool(b) => JsonValue::Bool(*b),
186 Value::Int(n) => JsonValue::Number(Number::from(*n)),
187 Value::Float(f) => Number::from_f64(*f).map_or(JsonValue::Null, JsonValue::Number),
188 Value::String(s) => JsonValue::String(s.to_string()),
189 Value::Tensor(t) => tensor_to_json(t),
190 Value::Reference(r) => {
191 json!({ "@ref": r.to_ref_string() })
193 }
194 Value::Expression(e) => {
195 JsonValue::String(format!("$({e})"))
197 }
198 }
199}
200
201fn tensor_to_json(tensor: &Tensor) -> JsonValue {
202 match tensor {
204 Tensor::Scalar(n) => Number::from_f64(*n).map_or(JsonValue::Null, JsonValue::Number),
205 Tensor::Array(items) => {
206 let mut arr = Vec::with_capacity(items.len());
209 for item in items {
210 arr.push(tensor_to_json(item));
211 }
212 JsonValue::Array(arr)
213 }
214 }
215}
216
217fn matrix_list_to_json(
218 list: &MatrixList,
219 doc: &Document,
220 config: &ToJsonConfig,
221) -> Result<JsonValue, String> {
222 let mut array = Vec::with_capacity(list.rows.len());
224
225 for row in &list.rows {
226 let mut row_obj = Map::with_capacity(list.schema.len() + 2); for (i, col_name) in list.schema.iter().enumerate() {
233 if let Some(field_value) = row.fields.get(i) {
234 row_obj.insert(col_name.clone(), value_to_json(field_value));
235 }
236 }
237
238 if config.include_metadata {
240 row_obj.insert(
241 "__type__".to_string(),
242 JsonValue::String(list.type_name.clone()),
243 );
244 }
245
246 if config.include_children {
248 if let Some(ref children) = row.children {
249 for (child_type, child_nodes) in children.as_ref() {
250 let child_json = nodes_to_json(child_type, child_nodes, doc, config)?;
251 row_obj.insert(child_type.clone(), child_json);
252 }
253 }
254 }
255
256 array.push(JsonValue::Object(row_obj));
257 }
258
259 if config.include_metadata && !config.flatten_lists {
261 let mut metadata = json!({
262 "__type__": list.type_name,
263 "__schema__": list.schema,
264 "items": array
265 });
266
267 if let Some(count) = list.count_hint {
269 if let Some(obj) = metadata.as_object_mut() {
270 obj.insert(
271 "__count_hint__".to_string(),
272 JsonValue::Number(count.into()),
273 );
274 }
275 }
276
277 Ok(metadata)
278 } else {
279 Ok(JsonValue::Array(array))
280 }
281}
282
283fn nodes_to_json(
284 type_name: &str,
285 nodes: &[Node],
286 doc: &Document,
287 config: &ToJsonConfig,
288) -> Result<JsonValue, String> {
289 let mut array = Vec::with_capacity(nodes.len());
292
293 let schema = doc.get_schema(type_name);
295
296 for node in nodes {
297 let capacity = if let Some(field_names) = schema {
299 field_names.len()
300 + usize::from(config.include_metadata)
301 + node.children.as_ref().map_or(0, |c| c.len())
302 } else {
303 node.fields.len()
304 + usize::from(config.include_metadata)
305 + node.children.as_ref().map_or(0, |c| c.len())
306 };
307 let mut obj = Map::with_capacity(capacity);
308
309 if let Some(field_names) = schema {
311 for (i, col_name) in field_names.iter().enumerate() {
312 if let Some(field_value) = node.fields.get(i) {
313 obj.insert(col_name.clone(), value_to_json(field_value));
314 }
315 }
316 } else {
317 obj.insert("id".to_string(), JsonValue::String(node.id.clone()));
319 for (i, value) in node.fields.iter().enumerate() {
320 obj.insert(format!("field_{i}"), value_to_json(value));
321 }
322 }
323
324 if config.include_metadata {
326 obj.insert(
327 "__type__".to_string(),
328 JsonValue::String(type_name.to_string()),
329 );
330 }
331
332 if config.include_children {
334 if let Some(ref children) = node.children {
335 for (child_type, child_nodes) in children.as_ref() {
336 let child_json = nodes_to_json(child_type, child_nodes, doc, config)?;
337 obj.insert(child_type.clone(), child_json);
338 }
339 }
340 }
341
342 array.push(JsonValue::Object(obj));
343 }
344
345 Ok(JsonValue::Array(array))
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use hedl_core::{Expression, Reference};
352
353 #[test]
356 fn test_to_json_config_default() {
357 let config = ToJsonConfig::default();
358 assert!(!config.include_metadata);
359 assert!(!config.flatten_lists);
360 assert!(config.include_children);
361 }
362
363 #[test]
364 fn test_to_json_config_debug() {
365 let config = ToJsonConfig::default();
366 let debug = format!("{config:?}");
367 assert!(debug.contains("ToJsonConfig"));
368 assert!(debug.contains("include_metadata"));
369 assert!(debug.contains("flatten_lists"));
370 assert!(debug.contains("include_children"));
371 }
372
373 #[test]
374 fn test_to_json_config_clone() {
375 let config = ToJsonConfig {
376 include_metadata: true,
377 flatten_lists: true,
378 include_children: false,
379 ascii_safe: true,
380 };
381 let cloned = config.clone();
382 assert!(cloned.include_metadata);
383 assert!(cloned.flatten_lists);
384 assert!(!cloned.include_children);
385 assert!(cloned.ascii_safe);
386 }
387
388 #[test]
391 fn test_value_to_json() {
392 assert_eq!(value_to_json(&Value::Null), JsonValue::Null);
393 assert_eq!(value_to_json(&Value::Bool(true)), JsonValue::Bool(true));
394 assert_eq!(value_to_json(&Value::Int(42)), json!(42));
395 assert_eq!(
396 value_to_json(&Value::String("hello".into())),
397 json!("hello")
398 );
399 }
400
401 #[test]
402 fn test_value_to_json_null() {
403 assert_eq!(value_to_json(&Value::Null), JsonValue::Null);
404 }
405
406 #[test]
407 fn test_value_to_json_bool() {
408 assert_eq!(value_to_json(&Value::Bool(true)), json!(true));
409 assert_eq!(value_to_json(&Value::Bool(false)), json!(false));
410 }
411
412 #[test]
413 fn test_value_to_json_int() {
414 assert_eq!(value_to_json(&Value::Int(0)), json!(0));
415 assert_eq!(value_to_json(&Value::Int(-42)), json!(-42));
416 assert_eq!(value_to_json(&Value::Int(i64::MAX)), json!(i64::MAX));
417 }
418
419 #[test]
420 fn test_value_to_json_float() {
421 assert_eq!(value_to_json(&Value::Float(3.5)), json!(3.5));
422 assert_eq!(value_to_json(&Value::Float(0.0)), json!(0.0));
423 assert_eq!(value_to_json(&Value::Float(-1.5)), json!(-1.5));
424 }
425
426 #[test]
427 fn test_value_to_json_float_nan() {
428 assert_eq!(value_to_json(&Value::Float(f64::NAN)), JsonValue::Null);
430 }
431
432 #[test]
433 fn test_value_to_json_float_infinity() {
434 assert_eq!(value_to_json(&Value::Float(f64::INFINITY)), JsonValue::Null);
436 assert_eq!(
437 value_to_json(&Value::Float(f64::NEG_INFINITY)),
438 JsonValue::Null
439 );
440 }
441
442 #[test]
443 fn test_value_to_json_string() {
444 assert_eq!(value_to_json(&Value::String("".into())), json!(""));
445 assert_eq!(
446 value_to_json(&Value::String("hello world".into())),
447 json!("hello world")
448 );
449 assert_eq!(
450 value_to_json(&Value::String("with\nnewline".into())),
451 json!("with\nnewline")
452 );
453 }
454
455 #[test]
456 fn test_value_to_json_string_unicode() {
457 assert_eq!(
458 value_to_json(&Value::String("héllo 世界".into())),
459 json!("héllo 世界")
460 );
461 }
462
463 #[test]
464 fn test_value_to_json_reference() {
465 let reference = Reference::qualified("User", "123");
466 let json = value_to_json(&Value::Reference(reference));
467 assert_eq!(json, json!({"@ref": "@User:123"}));
468 }
469
470 #[test]
471 fn test_value_to_json_reference_local() {
472 let reference = Reference::local("123");
473 let json = value_to_json(&Value::Reference(reference));
474 assert_eq!(json, json!({"@ref": "@123"}));
475 }
476
477 #[test]
478 fn test_value_to_json_expression() {
479 use hedl_core::lex::Span;
480 let expr = Expression::Identifier {
481 name: "foo".to_string(),
482 span: Span::synthetic(),
483 };
484 let json = value_to_json(&Value::Expression(Box::new(expr)));
485 assert_eq!(json, json!("$(foo)"));
486 }
487
488 #[test]
491 fn test_tensor_to_json_scalar() {
492 assert_eq!(tensor_to_json(&Tensor::Scalar(1.0)), json!(1.0));
493 assert_eq!(tensor_to_json(&Tensor::Scalar(3.5)), json!(3.5));
494 }
495
496 #[test]
497 fn test_tensor_to_json_1d() {
498 let tensor = Tensor::Array(vec![
499 Tensor::Scalar(1.0),
500 Tensor::Scalar(2.0),
501 Tensor::Scalar(3.0),
502 ]);
503 assert_eq!(tensor_to_json(&tensor), json!([1.0, 2.0, 3.0]));
504 }
505
506 #[test]
507 fn test_tensor_to_json_2d() {
508 let tensor = Tensor::Array(vec![
509 Tensor::Array(vec![Tensor::Scalar(1.0), Tensor::Scalar(2.0)]),
510 Tensor::Array(vec![Tensor::Scalar(3.0), Tensor::Scalar(4.0)]),
511 ]);
512 assert_eq!(tensor_to_json(&tensor), json!([[1.0, 2.0], [3.0, 4.0]]));
513 }
514
515 #[test]
516 fn test_tensor_to_json_empty() {
517 let tensor = Tensor::Array(vec![]);
518 assert_eq!(tensor_to_json(&tensor), json!([]));
519 }
520
521 #[test]
522 fn test_tensor_to_json_nan_becomes_null() {
523 let tensor = Tensor::Scalar(f64::NAN);
524 assert_eq!(tensor_to_json(&tensor), JsonValue::Null);
525 }
526
527 #[test]
530 fn test_item_to_json_scalar() {
531 let doc = Document::new((1, 0));
532 let config = ToJsonConfig::default();
533 let item = Item::Scalar(Value::Int(42));
534 let result = item_to_json(&item, &doc, &config).unwrap();
535 assert_eq!(result, json!(42));
536 }
537
538 #[test]
539 fn test_item_to_json_object() {
540 let doc = Document::new((1, 0));
541 let config = ToJsonConfig::default();
542 let mut obj = BTreeMap::new();
543 obj.insert(
544 "key".to_string(),
545 Item::Scalar(Value::String("value".into())),
546 );
547 let item = Item::Object(obj);
548 let result = item_to_json(&item, &doc, &config).unwrap();
549 assert_eq!(result, json!({"key": "value"}));
550 }
551
552 #[test]
555 fn test_object_to_json_empty() {
556 let doc = Document::new((1, 0));
557 let config = ToJsonConfig::default();
558 let obj = BTreeMap::new();
559 let result = object_to_json(&obj, &doc, &config).unwrap();
560 assert_eq!(result, json!({}));
561 }
562
563 #[test]
564 fn test_object_to_json_nested() {
565 let doc = Document::new((1, 0));
566 let config = ToJsonConfig::default();
567 let mut inner = BTreeMap::new();
568 inner.insert("nested".to_string(), Item::Scalar(Value::Bool(true)));
569 let mut outer = BTreeMap::new();
570 outer.insert("inner".to_string(), Item::Object(inner));
571 let result = object_to_json(&outer, &doc, &config).unwrap();
572 assert_eq!(result, json!({"inner": {"nested": true}}));
573 }
574
575 #[test]
578 fn test_root_to_json_empty() {
579 let doc = Document::new((1, 0));
580 let config = ToJsonConfig::default();
581 let root = BTreeMap::new();
582 let result = root_to_json(&root, &doc, &config).unwrap();
583 assert_eq!(result, json!({}));
584 }
585
586 #[test]
587 fn test_root_to_json_with_items() {
588 let doc = Document::new((1, 0));
589 let config = ToJsonConfig::default();
590 let mut root = BTreeMap::new();
591 root.insert(
592 "name".to_string(),
593 Item::Scalar(Value::String("test".into())),
594 );
595 root.insert("count".to_string(), Item::Scalar(Value::Int(42)));
596 let result = root_to_json(&root, &doc, &config).unwrap();
597 assert_eq!(result, json!({"name": "test", "count": 42}));
598 }
599
600 #[test]
603 fn test_to_json_empty_document() {
604 let doc = Document {
605 version: (1, 0),
606 aliases: BTreeMap::new(),
607 structs: BTreeMap::new(),
608 nests: BTreeMap::new(),
609 root: BTreeMap::new(),
610 schema_versions: BTreeMap::new(),
611 };
612 let config = ToJsonConfig::default();
613 let result = to_json(&doc, &config).unwrap();
614 assert_eq!(result.trim(), "{}");
615 }
616
617 #[test]
618 fn test_to_json_with_scalars() {
619 let mut root = BTreeMap::new();
620 root.insert(
621 "name".to_string(),
622 Item::Scalar(Value::String("test".into())),
623 );
624 root.insert("active".to_string(), Item::Scalar(Value::Bool(true)));
625 let doc = Document {
626 version: (1, 0),
627 aliases: BTreeMap::new(),
628 structs: BTreeMap::new(),
629 nests: BTreeMap::new(),
630 root,
631 schema_versions: BTreeMap::new(),
632 };
633 let config = ToJsonConfig::default();
634 let result = to_json(&doc, &config).unwrap();
635 let parsed: JsonValue = serde_json::from_str(&result).unwrap();
636 assert_eq!(parsed["name"], json!("test"));
637 assert_eq!(parsed["active"], json!(true));
638 }
639
640 #[test]
643 fn test_to_json_value_simple() {
644 let mut root = BTreeMap::new();
645 root.insert("key".to_string(), Item::Scalar(Value::Int(42)));
646 let doc = Document {
647 version: (1, 0),
648 aliases: BTreeMap::new(),
649 structs: BTreeMap::new(),
650 nests: BTreeMap::new(),
651 root,
652 schema_versions: BTreeMap::new(),
653 };
654 let config = ToJsonConfig::default();
655 let result = to_json_value(&doc, &config).unwrap();
656 assert_eq!(result, json!({"key": 42}));
657 }
658
659 #[test]
662 fn test_matrix_list_to_json_simple() {
663 let doc = Document::new((1, 0));
664 let config = ToJsonConfig::default();
665 let list = MatrixList {
666 type_name: "User".to_string(),
667 schema: vec!["id".to_string(), "name".to_string()],
668 rows: vec![Node {
669 type_name: "User".to_string(),
670 id: "1".to_string(),
671 fields: vec![Value::String("1".into()), Value::String("Alice".into())].into(),
672 children: None,
673 child_count: 0,
674 }],
675 count_hint: None,
676 };
677 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
678 assert_eq!(result, json!([{"id": "1", "name": "Alice"}]));
679 }
680
681 #[test]
682 fn test_matrix_list_to_json_with_metadata() {
683 let doc = Document::new((1, 0));
684 let config = ToJsonConfig {
685 include_metadata: true,
686 flatten_lists: false,
687 include_children: true,
688 ascii_safe: false,
689 };
690 let list = MatrixList {
691 type_name: "User".to_string(),
692 schema: vec!["id".to_string()],
693 rows: vec![Node {
694 type_name: "User".to_string(),
695 id: "1".to_string(),
696 fields: vec![Value::String("1".into())].into(),
697 children: None,
698 child_count: 0,
699 }],
700 count_hint: None,
701 };
702 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
703 assert!(result["__type__"] == json!("User"));
704 assert!(result["__schema__"] == json!(["id"]));
705 }
706
707 #[test]
708 fn test_matrix_list_to_json_empty() {
709 let doc = Document::new((1, 0));
710 let config = ToJsonConfig::default();
711 let list = MatrixList {
712 type_name: "User".to_string(),
713 schema: vec!["id".to_string()],
714 rows: vec![],
715 count_hint: None,
716 };
717 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
718 assert_eq!(result, json!([]));
719 }
720
721 #[test]
722 fn test_matrix_list_to_json_with_count_hint() {
723 let doc = Document::new((1, 0));
724 let config = ToJsonConfig {
725 include_metadata: true,
726 flatten_lists: false,
727 include_children: true,
728 ascii_safe: false,
729 };
730 let list = MatrixList {
731 type_name: "Team".to_string(),
732 schema: vec!["id".to_string(), "name".to_string()],
733 rows: vec![Node {
734 type_name: "Team".to_string(),
735 id: "1".to_string(),
736 fields: vec![Value::String("1".into()), Value::String("Alpha".into())].into(),
737 children: None,
738 child_count: 0,
739 }],
740 count_hint: Some(5),
741 };
742 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
743
744 assert_eq!(result["__count_hint__"], json!(5));
746 assert_eq!(result["__type__"], json!("Team"));
747 assert_eq!(result["__schema__"], json!(["id", "name"]));
748 }
749}