1use hedl_core::{Document, Item, MatrixList, Node, Value};
21use hedl_core::lex::Tensor;
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}
35
36impl Default for ToJsonConfig {
37 fn default() -> Self {
38 Self {
39 include_metadata: false,
40 flatten_lists: false,
41 include_children: true, }
43 }
44}
45
46impl hedl_core::convert::ExportConfig for ToJsonConfig {
47 fn include_metadata(&self) -> bool {
48 self.include_metadata
49 }
50
51 fn pretty(&self) -> bool {
52 true
54 }
55}
56
57pub fn to_json(doc: &Document, config: &ToJsonConfig) -> Result<String, String> {
59 let value = to_json_value(doc, config)?;
60 serde_json::to_string_pretty(&value).map_err(|e| format!("JSON serialization error: {}", e))
61}
62
63pub fn to_json_value(doc: &Document, config: &ToJsonConfig) -> Result<JsonValue, String> {
65 root_to_json(&doc.root, doc, config)
66}
67
68fn root_to_json(
69 root: &BTreeMap<String, Item>,
70 doc: &Document,
71 config: &ToJsonConfig,
72) -> Result<JsonValue, String> {
73 let mut map = Map::with_capacity(root.len());
75
76 for (key, item) in root {
77 let json_value = item_to_json(item, doc, config)?;
78 map.insert(key.clone(), json_value);
79 }
80
81 Ok(JsonValue::Object(map))
82}
83
84fn item_to_json(item: &Item, doc: &Document, config: &ToJsonConfig) -> Result<JsonValue, String> {
85 match item {
86 Item::Scalar(value) => Ok(value_to_json(value)),
87 Item::Object(obj) => object_to_json(obj, doc, config),
88 Item::List(list) => matrix_list_to_json(list, doc, config),
89 }
90}
91
92fn object_to_json(
93 obj: &BTreeMap<String, Item>,
94 doc: &Document,
95 config: &ToJsonConfig,
96) -> Result<JsonValue, String> {
97 let mut map = Map::with_capacity(obj.len());
99
100 for (key, item) in obj {
101 let json_value = item_to_json(item, doc, config)?;
102 map.insert(key.clone(), json_value);
103 }
104
105 Ok(JsonValue::Object(map))
106}
107
108fn value_to_json(value: &Value) -> JsonValue {
109 match value {
110 Value::Null => JsonValue::Null,
111 Value::Bool(b) => JsonValue::Bool(*b),
112 Value::Int(n) => JsonValue::Number(Number::from(*n)),
113 Value::Float(f) => Number::from_f64(*f)
114 .map(JsonValue::Number)
115 .unwrap_or(JsonValue::Null),
116 Value::String(s) => JsonValue::String(s.clone()),
117 Value::Tensor(t) => tensor_to_json(t),
118 Value::Reference(r) => {
119 json!({ "@ref": r.to_ref_string() })
121 }
122 Value::Expression(e) => {
123 JsonValue::String(format!("$({})", e))
125 }
126 }
127}
128
129fn tensor_to_json(tensor: &Tensor) -> JsonValue {
130 match tensor {
132 Tensor::Scalar(n) => Number::from_f64(*n)
133 .map(JsonValue::Number)
134 .unwrap_or(JsonValue::Null),
135 Tensor::Array(items) => {
136 let mut arr = Vec::with_capacity(items.len());
139 for item in items {
140 arr.push(tensor_to_json(item));
141 }
142 JsonValue::Array(arr)
143 }
144 }
145}
146
147fn matrix_list_to_json(
148 list: &MatrixList,
149 doc: &Document,
150 config: &ToJsonConfig,
151) -> Result<JsonValue, String> {
152 let mut array = Vec::with_capacity(list.rows.len());
154
155 for row in &list.rows {
156 let mut row_obj = Map::with_capacity(list.schema.len() + 2); for (i, col_name) in list.schema.iter().enumerate() {
163 if let Some(field_value) = row.fields.get(i) {
164 row_obj.insert(col_name.clone(), value_to_json(field_value));
165 }
166 }
167
168 if config.include_metadata {
170 row_obj.insert(
171 "__type__".to_string(),
172 JsonValue::String(list.type_name.clone()),
173 );
174 }
175
176 if config.include_children && !row.children.is_empty() {
178 for (child_type, child_nodes) in &row.children {
179 let child_json = nodes_to_json(child_type, child_nodes, doc, config)?;
180 row_obj.insert(child_type.clone(), child_json);
181 }
182 }
183
184 array.push(JsonValue::Object(row_obj));
185 }
186
187 if config.include_metadata && !config.flatten_lists {
189 let mut metadata = json!({
190 "__type__": list.type_name,
191 "__schema__": list.schema,
192 "items": array
193 });
194
195 if let Some(count) = list.count_hint {
197 if let Some(obj) = metadata.as_object_mut() {
198 obj.insert("__count_hint__".to_string(), JsonValue::Number(count.into()));
199 }
200 }
201
202 Ok(metadata)
203 } else {
204 Ok(JsonValue::Array(array))
205 }
206}
207
208fn nodes_to_json(
209 type_name: &str,
210 nodes: &[Node],
211 doc: &Document,
212 config: &ToJsonConfig,
213) -> Result<JsonValue, String> {
214 let mut array = Vec::with_capacity(nodes.len());
217
218 let schema = doc.get_schema(type_name);
220
221 for node in nodes {
222 let capacity = if let Some(field_names) = schema {
224 field_names.len() + if config.include_metadata { 1 } else { 0 } + node.children.len()
225 } else {
226 node.fields.len() + if config.include_metadata { 1 } else { 0 } + node.children.len()
227 };
228 let mut obj = Map::with_capacity(capacity);
229
230 if let Some(field_names) = schema {
232 for (i, col_name) in field_names.iter().enumerate() {
233 if let Some(field_value) = node.fields.get(i) {
234 obj.insert(col_name.clone(), value_to_json(field_value));
235 }
236 }
237 } else {
238 obj.insert("id".to_string(), JsonValue::String(node.id.clone()));
240 for (i, value) in node.fields.iter().enumerate() {
241 obj.insert(format!("field_{}", i), value_to_json(value));
242 }
243 }
244
245 if config.include_metadata {
247 obj.insert(
248 "__type__".to_string(),
249 JsonValue::String(type_name.to_string()),
250 );
251 }
252
253 if config.include_children && !node.children.is_empty() {
255 for (child_type, child_nodes) in &node.children {
256 let child_json = nodes_to_json(child_type, child_nodes, doc, config)?;
257 obj.insert(child_type.clone(), child_json);
258 }
259 }
260
261 array.push(JsonValue::Object(obj));
262 }
263
264 Ok(JsonValue::Array(array))
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use hedl_core::{Expression, Reference};
271
272 #[test]
275 fn test_to_json_config_default() {
276 let config = ToJsonConfig::default();
277 assert!(!config.include_metadata);
278 assert!(!config.flatten_lists);
279 assert!(config.include_children);
280 }
281
282 #[test]
283 fn test_to_json_config_debug() {
284 let config = ToJsonConfig::default();
285 let debug = format!("{:?}", config);
286 assert!(debug.contains("ToJsonConfig"));
287 assert!(debug.contains("include_metadata"));
288 assert!(debug.contains("flatten_lists"));
289 assert!(debug.contains("include_children"));
290 }
291
292 #[test]
293 fn test_to_json_config_clone() {
294 let config = ToJsonConfig {
295 include_metadata: true,
296 flatten_lists: true,
297 include_children: false,
298 };
299 let cloned = config.clone();
300 assert!(cloned.include_metadata);
301 assert!(cloned.flatten_lists);
302 assert!(!cloned.include_children);
303 }
304
305 #[test]
308 fn test_value_to_json() {
309 assert_eq!(value_to_json(&Value::Null), JsonValue::Null);
310 assert_eq!(value_to_json(&Value::Bool(true)), JsonValue::Bool(true));
311 assert_eq!(value_to_json(&Value::Int(42)), json!(42));
312 assert_eq!(
313 value_to_json(&Value::String("hello".into())),
314 json!("hello")
315 );
316 }
317
318 #[test]
319 fn test_value_to_json_null() {
320 assert_eq!(value_to_json(&Value::Null), JsonValue::Null);
321 }
322
323 #[test]
324 fn test_value_to_json_bool() {
325 assert_eq!(value_to_json(&Value::Bool(true)), json!(true));
326 assert_eq!(value_to_json(&Value::Bool(false)), json!(false));
327 }
328
329 #[test]
330 fn test_value_to_json_int() {
331 assert_eq!(value_to_json(&Value::Int(0)), json!(0));
332 assert_eq!(value_to_json(&Value::Int(-42)), json!(-42));
333 assert_eq!(value_to_json(&Value::Int(i64::MAX)), json!(i64::MAX));
334 }
335
336 #[test]
337 fn test_value_to_json_float() {
338 assert_eq!(value_to_json(&Value::Float(3.5)), json!(3.5));
339 assert_eq!(value_to_json(&Value::Float(0.0)), json!(0.0));
340 assert_eq!(value_to_json(&Value::Float(-1.5)), json!(-1.5));
341 }
342
343 #[test]
344 fn test_value_to_json_float_nan() {
345 assert_eq!(value_to_json(&Value::Float(f64::NAN)), JsonValue::Null);
347 }
348
349 #[test]
350 fn test_value_to_json_float_infinity() {
351 assert_eq!(value_to_json(&Value::Float(f64::INFINITY)), JsonValue::Null);
353 assert_eq!(
354 value_to_json(&Value::Float(f64::NEG_INFINITY)),
355 JsonValue::Null
356 );
357 }
358
359 #[test]
360 fn test_value_to_json_string() {
361 assert_eq!(value_to_json(&Value::String("".into())), json!(""));
362 assert_eq!(
363 value_to_json(&Value::String("hello world".into())),
364 json!("hello world")
365 );
366 assert_eq!(
367 value_to_json(&Value::String("with\nnewline".into())),
368 json!("with\nnewline")
369 );
370 }
371
372 #[test]
373 fn test_value_to_json_string_unicode() {
374 assert_eq!(
375 value_to_json(&Value::String("héllo 世界".into())),
376 json!("héllo 世界")
377 );
378 }
379
380 #[test]
381 fn test_value_to_json_reference() {
382 let reference = Reference::qualified("User", "123");
383 let json = value_to_json(&Value::Reference(reference));
384 assert_eq!(json, json!({"@ref": "@User:123"}));
385 }
386
387 #[test]
388 fn test_value_to_json_reference_local() {
389 let reference = Reference::local("123");
390 let json = value_to_json(&Value::Reference(reference));
391 assert_eq!(json, json!({"@ref": "@123"}));
392 }
393
394 #[test]
395 fn test_value_to_json_expression() {
396 use hedl_core::lex::Span;
397 let expr = Expression::Identifier {
398 name: "foo".to_string(),
399 span: Span::default(),
400 };
401 let json = value_to_json(&Value::Expression(expr));
402 assert_eq!(json, json!("$(foo)"));
403 }
404
405 #[test]
408 fn test_tensor_to_json_scalar() {
409 assert_eq!(tensor_to_json(&Tensor::Scalar(1.0)), json!(1.0));
410 assert_eq!(tensor_to_json(&Tensor::Scalar(3.5)), json!(3.5));
411 }
412
413 #[test]
414 fn test_tensor_to_json_1d() {
415 let tensor = Tensor::Array(vec![
416 Tensor::Scalar(1.0),
417 Tensor::Scalar(2.0),
418 Tensor::Scalar(3.0),
419 ]);
420 assert_eq!(tensor_to_json(&tensor), json!([1.0, 2.0, 3.0]));
421 }
422
423 #[test]
424 fn test_tensor_to_json_2d() {
425 let tensor = Tensor::Array(vec![
426 Tensor::Array(vec![Tensor::Scalar(1.0), Tensor::Scalar(2.0)]),
427 Tensor::Array(vec![Tensor::Scalar(3.0), Tensor::Scalar(4.0)]),
428 ]);
429 assert_eq!(tensor_to_json(&tensor), json!([[1.0, 2.0], [3.0, 4.0]]));
430 }
431
432 #[test]
433 fn test_tensor_to_json_empty() {
434 let tensor = Tensor::Array(vec![]);
435 assert_eq!(tensor_to_json(&tensor), json!([]));
436 }
437
438 #[test]
439 fn test_tensor_to_json_nan_becomes_null() {
440 let tensor = Tensor::Scalar(f64::NAN);
441 assert_eq!(tensor_to_json(&tensor), JsonValue::Null);
442 }
443
444 #[test]
447 fn test_item_to_json_scalar() {
448 let doc = Document::new((1, 0));
449 let config = ToJsonConfig::default();
450 let item = Item::Scalar(Value::Int(42));
451 let result = item_to_json(&item, &doc, &config).unwrap();
452 assert_eq!(result, json!(42));
453 }
454
455 #[test]
456 fn test_item_to_json_object() {
457 let doc = Document::new((1, 0));
458 let config = ToJsonConfig::default();
459 let mut obj = BTreeMap::new();
460 obj.insert(
461 "key".to_string(),
462 Item::Scalar(Value::String("value".into())),
463 );
464 let item = Item::Object(obj);
465 let result = item_to_json(&item, &doc, &config).unwrap();
466 assert_eq!(result, json!({"key": "value"}));
467 }
468
469 #[test]
472 fn test_object_to_json_empty() {
473 let doc = Document::new((1, 0));
474 let config = ToJsonConfig::default();
475 let obj = BTreeMap::new();
476 let result = object_to_json(&obj, &doc, &config).unwrap();
477 assert_eq!(result, json!({}));
478 }
479
480 #[test]
481 fn test_object_to_json_nested() {
482 let doc = Document::new((1, 0));
483 let config = ToJsonConfig::default();
484 let mut inner = BTreeMap::new();
485 inner.insert("nested".to_string(), Item::Scalar(Value::Bool(true)));
486 let mut outer = BTreeMap::new();
487 outer.insert("inner".to_string(), Item::Object(inner));
488 let result = object_to_json(&outer, &doc, &config).unwrap();
489 assert_eq!(result, json!({"inner": {"nested": true}}));
490 }
491
492 #[test]
495 fn test_root_to_json_empty() {
496 let doc = Document::new((1, 0));
497 let config = ToJsonConfig::default();
498 let root = BTreeMap::new();
499 let result = root_to_json(&root, &doc, &config).unwrap();
500 assert_eq!(result, json!({}));
501 }
502
503 #[test]
504 fn test_root_to_json_with_items() {
505 let doc = Document::new((1, 0));
506 let config = ToJsonConfig::default();
507 let mut root = BTreeMap::new();
508 root.insert(
509 "name".to_string(),
510 Item::Scalar(Value::String("test".into())),
511 );
512 root.insert("count".to_string(), Item::Scalar(Value::Int(42)));
513 let result = root_to_json(&root, &doc, &config).unwrap();
514 assert_eq!(result, json!({"name": "test", "count": 42}));
515 }
516
517 #[test]
520 fn test_to_json_empty_document() {
521 let doc = Document {
522 version: (1, 0),
523 aliases: BTreeMap::new(),
524 structs: BTreeMap::new(),
525 nests: BTreeMap::new(),
526 root: BTreeMap::new(),
527 };
528 let config = ToJsonConfig::default();
529 let result = to_json(&doc, &config).unwrap();
530 assert_eq!(result.trim(), "{}");
531 }
532
533 #[test]
534 fn test_to_json_with_scalars() {
535 let mut root = BTreeMap::new();
536 root.insert(
537 "name".to_string(),
538 Item::Scalar(Value::String("test".into())),
539 );
540 root.insert("active".to_string(), Item::Scalar(Value::Bool(true)));
541 let doc = Document {
542 version: (1, 0),
543 aliases: BTreeMap::new(),
544 structs: BTreeMap::new(),
545 nests: BTreeMap::new(),
546 root,
547 };
548 let config = ToJsonConfig::default();
549 let result = to_json(&doc, &config).unwrap();
550 let parsed: JsonValue = serde_json::from_str(&result).unwrap();
551 assert_eq!(parsed["name"], json!("test"));
552 assert_eq!(parsed["active"], json!(true));
553 }
554
555 #[test]
558 fn test_to_json_value_simple() {
559 let mut root = BTreeMap::new();
560 root.insert("key".to_string(), Item::Scalar(Value::Int(42)));
561 let doc = Document {
562 version: (1, 0),
563 aliases: BTreeMap::new(),
564 structs: BTreeMap::new(),
565 nests: BTreeMap::new(),
566 root,
567 };
568 let config = ToJsonConfig::default();
569 let result = to_json_value(&doc, &config).unwrap();
570 assert_eq!(result, json!({"key": 42}));
571 }
572
573 #[test]
576 fn test_matrix_list_to_json_simple() {
577 let doc = Document::new((1, 0));
578 let config = ToJsonConfig::default();
579 let list = MatrixList {
580 type_name: "User".to_string(),
581 schema: vec!["id".to_string(), "name".to_string()],
582 rows: vec![Node {
583 type_name: "User".to_string(),
584 id: "1".to_string(),
585 fields: vec![Value::String("1".into()), Value::String("Alice".into())],
586 children: BTreeMap::new(),
587 child_count: None,
588 }],
589 count_hint: None,
590 };
591 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
592 assert_eq!(result, json!([{"id": "1", "name": "Alice"}]));
593 }
594
595 #[test]
596 fn test_matrix_list_to_json_with_metadata() {
597 let doc = Document::new((1, 0));
598 let config = ToJsonConfig {
599 include_metadata: true,
600 flatten_lists: false,
601 include_children: true,
602 };
603 let list = MatrixList {
604 type_name: "User".to_string(),
605 schema: vec!["id".to_string()],
606 rows: vec![Node {
607 type_name: "User".to_string(),
608 id: "1".to_string(),
609 fields: vec![Value::String("1".into())],
610 children: BTreeMap::new(),
611 child_count: None,
612 }],
613 count_hint: None,
614 };
615 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
616 assert!(result["__type__"] == json!("User"));
617 assert!(result["__schema__"] == json!(["id"]));
618 }
619
620 #[test]
621 fn test_matrix_list_to_json_empty() {
622 let doc = Document::new((1, 0));
623 let config = ToJsonConfig::default();
624 let list = MatrixList {
625 type_name: "User".to_string(),
626 schema: vec!["id".to_string()],
627 rows: vec![],
628 count_hint: None,
629 };
630 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
631 assert_eq!(result, json!([]));
632 }
633
634 #[test]
635 fn test_matrix_list_to_json_with_count_hint() {
636 let doc = Document::new((1, 0));
637 let config = ToJsonConfig {
638 include_metadata: true,
639 flatten_lists: false,
640 include_children: true,
641 };
642 let list = MatrixList {
643 type_name: "Team".to_string(),
644 schema: vec!["id".to_string(), "name".to_string()],
645 rows: vec![Node {
646 type_name: "Team".to_string(),
647 id: "1".to_string(),
648 fields: vec![Value::String("1".into()), Value::String("Alpha".into())],
649 children: BTreeMap::new(),
650 child_count: None,
651 }],
652 count_hint: Some(5),
653 };
654 let result = matrix_list_to_json(&list, &doc, &config).unwrap();
655
656 assert_eq!(result["__count_hint__"], json!(5));
658 assert_eq!(result["__type__"], json!("Team"));
659 assert_eq!(result["__schema__"], json!(["id", "name"]));
660 }
661}