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 Value::List(items) => {
199 let mut arr = Vec::with_capacity(items.len());
201 for item in items.as_ref() {
202 arr.push(value_to_json(item));
203 }
204 JsonValue::Array(arr)
205 }
206 }
207}
208
209fn tensor_to_json(tensor: &Tensor) -> JsonValue {
210 match tensor {
212 Tensor::Scalar(n) => Number::from_f64(*n).map_or(JsonValue::Null, JsonValue::Number),
213 Tensor::Array(items) => {
214 let mut arr = Vec::with_capacity(items.len());
217 for item in items {
218 arr.push(tensor_to_json(item));
219 }
220 JsonValue::Array(arr)
221 }
222 }
223}
224
225fn matrix_list_to_json(
226 list: &MatrixList,
227 doc: &Document,
228 config: &ToJsonConfig,
229) -> Result<JsonValue, String> {
230 let mut array = Vec::with_capacity(list.rows.len());
232
233 for row in &list.rows {
234 let mut row_obj = Map::with_capacity(list.schema.len() + 2); for (i, col_name) in list.schema.iter().enumerate() {
241 if let Some(field_value) = row.fields.get(i) {
242 row_obj.insert(col_name.clone(), value_to_json(field_value));
243 }
244 }
245
246 if config.include_metadata {
248 row_obj.insert(
249 "__type__".to_string(),
250 JsonValue::String(list.type_name.clone()),
251 );
252 }
253
254 if config.include_children {
256 if let Some(ref children) = row.children {
257 for (child_type, child_nodes) in children.as_ref() {
258 let child_json = nodes_to_json(child_type, child_nodes, doc, config)?;
259 row_obj.insert(child_type.clone(), child_json);
260 }
261 }
262 }
263
264 array.push(JsonValue::Object(row_obj));
265 }
266
267 if config.include_metadata && !config.flatten_lists {
269 let mut metadata = json!({
270 "__type__": list.type_name,
271 "__schema__": list.schema,
272 "items": array
273 });
274
275 if let Some(count) = list.count_hint {
277 if let Some(obj) = metadata.as_object_mut() {
278 obj.insert(
279 "__count_hint__".to_string(),
280 JsonValue::Number(count.into()),
281 );
282 }
283 }
284
285 Ok(metadata)
286 } else {
287 Ok(JsonValue::Array(array))
288 }
289}
290
291fn nodes_to_json(
292 type_name: &str,
293 nodes: &[Node],
294 doc: &Document,
295 config: &ToJsonConfig,
296) -> Result<JsonValue, String> {
297 let mut array = Vec::with_capacity(nodes.len());
300
301 let schema = doc.get_schema(type_name);
303
304 for node in nodes {
305 let capacity = if let Some(field_names) = schema {
307 field_names.len()
308 + usize::from(config.include_metadata)
309 + node.children.as_ref().map_or(0, |c| c.len())
310 } else {
311 node.fields.len()
312 + usize::from(config.include_metadata)
313 + node.children.as_ref().map_or(0, |c| c.len())
314 };
315 let mut obj = Map::with_capacity(capacity);
316
317 if let Some(field_names) = schema {
319 for (i, col_name) in field_names.iter().enumerate() {
320 if let Some(field_value) = node.fields.get(i) {
321 obj.insert(col_name.clone(), value_to_json(field_value));
322 }
323 }
324 } else {
325 obj.insert("id".to_string(), JsonValue::String(node.id.clone()));
327 for (i, value) in node.fields.iter().enumerate() {
328 obj.insert(format!("field_{i}"), value_to_json(value));
329 }
330 }
331
332 if config.include_metadata {
334 obj.insert(
335 "__type__".to_string(),
336 JsonValue::String(type_name.to_string()),
337 );
338 }
339
340 if config.include_children {
342 if let Some(ref children) = node.children {
343 for (child_type, child_nodes) in children.as_ref() {
344 let child_json = nodes_to_json(child_type, child_nodes, doc, config)?;
345 obj.insert(child_type.clone(), child_json);
346 }
347 }
348 }
349
350 array.push(JsonValue::Object(obj));
351 }
352
353 Ok(JsonValue::Array(array))
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use hedl_core::{Expression, Reference};
360
361 #[test]
364 fn test_to_json_config_default() {
365 let config = ToJsonConfig::default();
366 assert!(!config.include_metadata);
367 assert!(!config.flatten_lists);
368 assert!(config.include_children);
369 }
370
371 #[test]
372 fn test_to_json_config_debug() {
373 let config = ToJsonConfig::default();
374 let debug = format!("{config:?}");
375 assert!(debug.contains("ToJsonConfig"));
376 assert!(debug.contains("include_metadata"));
377 assert!(debug.contains("flatten_lists"));
378 assert!(debug.contains("include_children"));
379 }
380
381 #[test]
382 fn test_to_json_config_clone() {
383 let config = ToJsonConfig {
384 include_metadata: true,
385 flatten_lists: true,
386 include_children: false,
387 ascii_safe: true,
388 };
389 let cloned = config.clone();
390 assert!(cloned.include_metadata);
391 assert!(cloned.flatten_lists);
392 assert!(!cloned.include_children);
393 assert!(cloned.ascii_safe);
394 }
395
396 #[test]
399 fn test_value_to_json() {
400 assert_eq!(value_to_json(&Value::Null), JsonValue::Null);
401 assert_eq!(value_to_json(&Value::Bool(true)), JsonValue::Bool(true));
402 assert_eq!(value_to_json(&Value::Int(42)), json!(42));
403 assert_eq!(
404 value_to_json(&Value::String("hello".into())),
405 json!("hello")
406 );
407 }
408
409 #[test]
410 fn test_value_to_json_null() {
411 assert_eq!(value_to_json(&Value::Null), JsonValue::Null);
412 }
413
414 #[test]
415 fn test_value_to_json_bool() {
416 assert_eq!(value_to_json(&Value::Bool(true)), json!(true));
417 assert_eq!(value_to_json(&Value::Bool(false)), json!(false));
418 }
419
420 #[test]
421 fn test_value_to_json_int() {
422 assert_eq!(value_to_json(&Value::Int(0)), json!(0));
423 assert_eq!(value_to_json(&Value::Int(-42)), json!(-42));
424 assert_eq!(value_to_json(&Value::Int(i64::MAX)), json!(i64::MAX));
425 }
426
427 #[test]
428 fn test_value_to_json_float() {
429 assert_eq!(value_to_json(&Value::Float(3.5)), json!(3.5));
430 assert_eq!(value_to_json(&Value::Float(0.0)), json!(0.0));
431 assert_eq!(value_to_json(&Value::Float(-1.5)), json!(-1.5));
432 }
433
434 #[test]
435 fn test_value_to_json_float_nan() {
436 assert_eq!(value_to_json(&Value::Float(f64::NAN)), JsonValue::Null);
438 }
439
440 #[test]
441 fn test_value_to_json_float_infinity() {
442 assert_eq!(value_to_json(&Value::Float(f64::INFINITY)), JsonValue::Null);
444 assert_eq!(
445 value_to_json(&Value::Float(f64::NEG_INFINITY)),
446 JsonValue::Null
447 );
448 }
449
450 #[test]
451 fn test_value_to_json_string() {
452 assert_eq!(value_to_json(&Value::String("".into())), json!(""));
453 assert_eq!(
454 value_to_json(&Value::String("hello world".into())),
455 json!("hello world")
456 );
457 assert_eq!(
458 value_to_json(&Value::String("with\nnewline".into())),
459 json!("with\nnewline")
460 );
461 }
462
463 #[test]
464 fn test_value_to_json_string_unicode() {
465 assert_eq!(
466 value_to_json(&Value::String("héllo 世界".into())),
467 json!("héllo 世界")
468 );
469 }
470
471 #[test]
472 fn test_value_to_json_reference() {
473 let reference = Reference::qualified("User", "123");
474 let json = value_to_json(&Value::Reference(reference));
475 assert_eq!(json, json!({"@ref": "@User:123"}));
476 }
477
478 #[test]
479 fn test_value_to_json_reference_local() {
480 let reference = Reference::local("123");
481 let json = value_to_json(&Value::Reference(reference));
482 assert_eq!(json, json!({"@ref": "@123"}));
483 }
484
485 #[test]
486 fn test_value_to_json_expression() {
487 use hedl_core::lex::Span;
488 let expr = Expression::Identifier {
489 name: "foo".to_string(),
490 span: Span::synthetic(),
491 };
492 let json = value_to_json(&Value::Expression(Box::new(expr)));
493 assert_eq!(json, json!("$(foo)"));
494 }
495
496 #[test]
499 fn test_tensor_to_json_scalar() {
500 assert_eq!(tensor_to_json(&Tensor::Scalar(1.0)), json!(1.0));
501 assert_eq!(tensor_to_json(&Tensor::Scalar(3.5)), json!(3.5));
502 }
503
504 #[test]
505 fn test_tensor_to_json_1d() {
506 let tensor = Tensor::Array(vec![
507 Tensor::Scalar(1.0),
508 Tensor::Scalar(2.0),
509 Tensor::Scalar(3.0),
510 ]);
511 assert_eq!(tensor_to_json(&tensor), json!([1.0, 2.0, 3.0]));
512 }
513
514 #[test]
515 fn test_tensor_to_json_2d() {
516 let tensor = Tensor::Array(vec![
517 Tensor::Array(vec![Tensor::Scalar(1.0), Tensor::Scalar(2.0)]),
518 Tensor::Array(vec![Tensor::Scalar(3.0), Tensor::Scalar(4.0)]),
519 ]);
520 assert_eq!(tensor_to_json(&tensor), json!([[1.0, 2.0], [3.0, 4.0]]));
521 }
522
523 #[test]
524 fn test_tensor_to_json_empty() {
525 let tensor = Tensor::Array(vec![]);
526 assert_eq!(tensor_to_json(&tensor), json!([]));
527 }
528
529 #[test]
530 fn test_tensor_to_json_nan_becomes_null() {
531 let tensor = Tensor::Scalar(f64::NAN);
532 assert_eq!(tensor_to_json(&tensor), JsonValue::Null);
533 }
534
535 #[test]
538 fn test_item_to_json_scalar() {
539 let doc = Document::new((2, 0));
540 let config = ToJsonConfig::default();
541 let item = Item::Scalar(Value::Int(42));
542 let result = item_to_json(&item, &doc, &config).unwrap();
543 assert_eq!(result, json!(42));
544 }
545
546 #[test]
547 fn test_item_to_json_object() {
548 let doc = Document::new((2, 0));
549 let config = ToJsonConfig::default();
550 let mut obj = BTreeMap::new();
551 obj.insert(
552 "key".to_string(),
553 Item::Scalar(Value::String("value".into())),
554 );
555 let item = Item::Object(obj);
556 let result = item_to_json(&item, &doc, &config).unwrap();
557 assert_eq!(result, json!({"key": "value"}));
558 }
559
560 #[test]
563 fn test_object_to_json_empty() {
564 let doc = Document::new((2, 0));
565 let config = ToJsonConfig::default();
566 let obj = BTreeMap::new();
567 let result = object_to_json(&obj, &doc, &config).unwrap();
568 assert_eq!(result, json!({}));
569 }
570
571 #[test]
572 fn test_object_to_json_nested() {
573 let doc = Document::new((2, 0));
574 let config = ToJsonConfig::default();
575 let mut inner = BTreeMap::new();
576 inner.insert("nested".to_string(), Item::Scalar(Value::Bool(true)));
577 let mut outer = BTreeMap::new();
578 outer.insert("inner".to_string(), Item::Object(inner));
579 let result = object_to_json(&outer, &doc, &config).unwrap();
580 assert_eq!(result, json!({"inner": {"nested": true}}));
581 }
582
583 #[test]
586 fn test_root_to_json_empty() {
587 let doc = Document::new((2, 0));
588 let config = ToJsonConfig::default();
589 let root = BTreeMap::new();
590 let result = root_to_json(&root, &doc, &config).unwrap();
591 assert_eq!(result, json!({}));
592 }
593
594 #[test]
595 fn test_root_to_json_with_items() {
596 let doc = Document::new((2, 0));
597 let config = ToJsonConfig::default();
598 let mut root = BTreeMap::new();
599 root.insert(
600 "name".to_string(),
601 Item::Scalar(Value::String("test".into())),
602 );
603 root.insert("count".to_string(), Item::Scalar(Value::Int(42)));
604 let result = root_to_json(&root, &doc, &config).unwrap();
605 assert_eq!(result, json!({"name": "test", "count": 42}));
606 }
607
608 #[test]
611 fn test_to_json_empty_document() {
612 let doc = Document {
613 version: (1, 0),
614 aliases: BTreeMap::new(),
615 structs: BTreeMap::new(),
616 nests: BTreeMap::new(),
617 root: BTreeMap::new(),
618 schema_versions: BTreeMap::new(),
619 };
620 let config = ToJsonConfig::default();
621 let result = to_json(&doc, &config).unwrap();
622 assert_eq!(result.trim(), "{}");
623 }
624
625 #[test]
626 fn test_to_json_with_scalars() {
627 let mut root = BTreeMap::new();
628 root.insert(
629 "name".to_string(),
630 Item::Scalar(Value::String("test".into())),
631 );
632 root.insert("active".to_string(), Item::Scalar(Value::Bool(true)));
633 let doc = Document {
634 version: (1, 0),
635 aliases: BTreeMap::new(),
636 structs: BTreeMap::new(),
637 nests: BTreeMap::new(),
638 root,
639 schema_versions: BTreeMap::new(),
640 };
641 let config = ToJsonConfig::default();
642 let result = to_json(&doc, &config).unwrap();
643 let parsed: JsonValue = serde_json::from_str(&result).unwrap();
644 assert_eq!(parsed["name"], json!("test"));
645 assert_eq!(parsed["active"], json!(true));
646 }
647
648 #[test]
651 fn test_to_json_value_simple() {
652 let mut root = BTreeMap::new();
653 root.insert("key".to_string(), Item::Scalar(Value::Int(42)));
654 let doc = Document {
655 version: (1, 0),
656 aliases: BTreeMap::new(),
657 structs: BTreeMap::new(),
658 nests: BTreeMap::new(),
659 root,
660 schema_versions: BTreeMap::new(),
661 };
662 let config = ToJsonConfig::default();
663 let result = to_json_value(&doc, &config).unwrap();
664 assert_eq!(result, json!({"key": 42}));
665 }
666
667 #[test]
670 fn test_matrix_list_to_json_simple() {
671 let doc = Document::new((2, 0));
672 let config = ToJsonConfig::default();
673 let list = MatrixList {
674 type_name: "User".to_string(),
675 schema: vec!["id".to_string(), "name".to_string()],
676 rows: vec![Node {
677 type_name: "User".to_string(),
678 id: "1".to_string(),
679 fields: vec![Value::String("1".into()), Value::String("Alice".into())].into(),
680 children: None,
681 child_count: 0,
682 }],
683 count_hint: None,
684 };
685 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
686 assert_eq!(result, json!([{"id": "1", "name": "Alice"}]));
687 }
688
689 #[test]
690 fn test_matrix_list_to_json_with_metadata() {
691 let doc = Document::new((2, 0));
692 let config = ToJsonConfig {
693 include_metadata: true,
694 flatten_lists: false,
695 include_children: true,
696 ascii_safe: false,
697 };
698 let list = MatrixList {
699 type_name: "User".to_string(),
700 schema: vec!["id".to_string()],
701 rows: vec![Node {
702 type_name: "User".to_string(),
703 id: "1".to_string(),
704 fields: vec![Value::String("1".into())].into(),
705 children: None,
706 child_count: 0,
707 }],
708 count_hint: None,
709 };
710 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
711 assert!(result["__type__"] == json!("User"));
712 assert!(result["__schema__"] == json!(["id"]));
713 }
714
715 #[test]
716 fn test_matrix_list_to_json_empty() {
717 let doc = Document::new((2, 0));
718 let config = ToJsonConfig::default();
719 let list = MatrixList {
720 type_name: "User".to_string(),
721 schema: vec!["id".to_string()],
722 rows: vec![],
723 count_hint: None,
724 };
725 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
726 assert_eq!(result, json!([]));
727 }
728
729 #[test]
730 fn test_matrix_list_to_json_with_count_hint() {
731 let doc = Document::new((2, 0));
732 let config = ToJsonConfig {
733 include_metadata: true,
734 flatten_lists: false,
735 include_children: true,
736 ascii_safe: false,
737 };
738 let list = MatrixList {
739 type_name: "Team".to_string(),
740 schema: vec!["id".to_string(), "name".to_string()],
741 rows: vec![Node {
742 type_name: "Team".to_string(),
743 id: "1".to_string(),
744 fields: vec![Value::String("1".into()), Value::String("Alpha".into())].into(),
745 children: None,
746 child_count: 0,
747 }],
748 count_hint: Some(5),
749 };
750 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
751
752 assert_eq!(result["__count_hint__"], json!(5));
754 assert_eq!(result["__type__"], json!("Team"));
755 assert_eq!(result["__schema__"], json!(["id", "name"]));
756 }
757}