1use crate::error::{CsvError, Result};
21use hedl_core::{Document, MatrixList, Tensor, Value};
22use std::io::Write;
23
24#[derive(Debug, Clone)]
26pub struct ToCsvConfig {
27 pub delimiter: u8,
29 pub include_headers: bool,
31 pub quote_style: csv::QuoteStyle,
33}
34
35impl Default for ToCsvConfig {
36 fn default() -> Self {
37 Self {
38 delimiter: b',',
39 include_headers: true,
40 quote_style: csv::QuoteStyle::Necessary,
41 }
42 }
43}
44
45pub fn to_csv(doc: &Document) -> Result<String> {
56 to_csv_with_config(doc, ToCsvConfig::default())
57}
58
59pub fn to_csv_list(doc: &Document, list_name: &str) -> Result<String> {
91 to_csv_list_with_config(doc, list_name, ToCsvConfig::default())
92}
93
94pub fn to_csv_list_with_config(
116 doc: &Document,
117 list_name: &str,
118 config: ToCsvConfig,
119) -> Result<String> {
120 let estimated_size = estimate_list_csv_size(doc, list_name);
121 let mut buffer = Vec::with_capacity(estimated_size);
122
123 to_csv_list_writer_with_config(doc, list_name, &mut buffer, config)?;
124 String::from_utf8(buffer).map_err(|_| CsvError::InvalidUtf8 {
125 context: "CSV output".to_string(),
126 })
127}
128
129pub fn to_csv_list_writer<W: Write>(doc: &Document, list_name: &str, writer: W) -> Result<()> {
148 to_csv_list_writer_with_config(doc, list_name, writer, ToCsvConfig::default())
149}
150
151pub fn to_csv_list_writer_with_config<W: Write>(
160 doc: &Document,
161 list_name: &str,
162 writer: W,
163 config: ToCsvConfig,
164) -> Result<()> {
165 let matrix_list = find_matrix_list_by_name(doc, list_name)?;
167
168 let mut wtr = csv::WriterBuilder::new()
169 .delimiter(config.delimiter)
170 .quote_style(config.quote_style)
171 .from_writer(writer);
172
173 if config.include_headers {
175 wtr.write_record(&matrix_list.schema).map_err(|e| {
176 CsvError::Other(format!(
177 "Failed to write CSV header for list '{list_name}': {e}"
178 ))
179 })?;
180 }
181
182 for node in &matrix_list.rows {
184 let record: Vec<String> = node.fields.iter().map(value_to_csv_string).collect();
185
186 wtr.write_record(&record).map_err(|e| {
187 CsvError::Other(format!(
188 "Failed to write CSV record for id '{}' in list '{}': {}",
189 node.id, list_name, e
190 ))
191 })?;
192
193 }
196
197 wtr.flush().map_err(|e| {
198 CsvError::Other(format!(
199 "Failed to flush CSV writer for list '{list_name}': {e}"
200 ))
201 })?;
202
203 Ok(())
204}
205
206pub fn to_csv_with_config(doc: &Document, config: ToCsvConfig) -> Result<String> {
209 let estimated_size = estimate_csv_size(doc);
212 let mut buffer = Vec::with_capacity(estimated_size);
213
214 to_csv_writer_with_config(doc, &mut buffer, config)?;
215 String::from_utf8(buffer).map_err(|_| CsvError::InvalidUtf8 {
216 context: "CSV output".to_string(),
217 })
218}
219
220fn estimate_csv_size(doc: &Document) -> usize {
222 let mut total = 0;
223
224 for item in doc.root.values() {
226 if let Some(list) = item.as_list() {
227 let header_size = list
229 .schema
230 .iter()
231 .map(std::string::String::len)
232 .sum::<usize>()
233 + list.schema.len()
234 + 1;
235
236 let row_count = list.rows.len();
238 let col_count = list.schema.len();
239 let data_size = row_count * col_count * 20;
240
241 total += header_size + data_size;
242 }
243 }
244
245 total.max(1024)
247}
248
249fn estimate_list_csv_size(doc: &Document, list_name: &str) -> usize {
251 if let Some(item) = doc.root.get(list_name) {
252 if let Some(list) = item.as_list() {
253 let header_size = list
255 .schema
256 .iter()
257 .map(std::string::String::len)
258 .sum::<usize>()
259 + list.schema.len()
260 + 1;
261
262 let row_count = list.rows.len();
264 let col_count = list.schema.len();
265 let data_size = row_count * col_count * 20;
266
267 return (header_size + data_size).max(1024);
268 }
269 }
270
271 1024
273}
274
275pub fn to_csv_writer<W: Write>(doc: &Document, writer: W) -> Result<()> {
288 to_csv_writer_with_config(doc, writer, ToCsvConfig::default())
289}
290
291pub fn to_csv_writer_with_config<W: Write>(
293 doc: &Document,
294 writer: W,
295 config: ToCsvConfig,
296) -> Result<()> {
297 let mut wtr = csv::WriterBuilder::new()
298 .delimiter(config.delimiter)
299 .quote_style(config.quote_style)
300 .from_writer(writer);
301
302 let matrix_list = find_first_matrix_list(doc)?;
304
305 if config.include_headers {
308 wtr.write_record(&matrix_list.schema)
309 .map_err(|e| CsvError::Other(format!("Failed to write CSV header: {e}")))?;
310 }
311
312 for node in &matrix_list.rows {
315 let record: Vec<String> = node.fields.iter().map(value_to_csv_string).collect();
316
317 wtr.write_record(&record).map_err(|e| {
318 CsvError::Other(format!(
319 "Failed to write CSV record for id '{}': {}",
320 node.id, e
321 ))
322 })?;
323 }
324
325 wtr.flush()
326 .map_err(|e| CsvError::Other(format!("Failed to flush CSV writer: {e}")))?;
327
328 Ok(())
329}
330
331fn find_first_matrix_list(doc: &Document) -> Result<&MatrixList> {
333 for item in doc.root.values() {
334 if let Some(list) = item.as_list() {
335 return Ok(list);
336 }
337 }
338
339 Err(CsvError::NoLists)
340}
341
342fn find_matrix_list_by_name<'a>(doc: &'a Document, list_name: &str) -> Result<&'a MatrixList> {
344 match doc.root.get(list_name) {
345 Some(item) => match item.as_list() {
346 Some(list) => Ok(list),
347 None => Err(CsvError::NotAList {
348 name: list_name.to_string(),
349 actual_type: match item {
350 hedl_core::Item::Scalar(_) => "scalar",
351 hedl_core::Item::Object(_) => "object",
352 hedl_core::Item::List(_) => "list",
353 }
354 .to_string(),
355 }),
356 },
357 None => Err(CsvError::ListNotFound {
358 name: list_name.to_string(),
359 available: if doc.root.is_empty() {
360 "none".to_string()
361 } else {
362 doc.root
363 .keys()
364 .map(|k| format!("'{k}'"))
365 .collect::<Vec<_>>()
366 .join(", ")
367 },
368 }),
369 }
370}
371
372fn value_to_csv_string(value: &Value) -> String {
374 match value {
375 Value::Null => String::new(),
376 Value::Bool(b) => b.to_string(),
377 Value::Int(n) => n.to_string(),
378 Value::Float(f) => {
379 if f.is_nan() {
381 "NaN".to_string()
382 } else if f.is_infinite() {
383 if f.is_sign_positive() {
384 "Infinity".to_string()
385 } else {
386 "-Infinity".to_string()
387 }
388 } else {
389 f.to_string()
390 }
391 }
392 Value::String(s) => s.to_string(),
393 Value::Reference(r) => r.to_ref_string(),
394 Value::Tensor(t) => tensor_to_json_string(t),
395 Value::Expression(e) => format!("$({e})"),
396 }
397}
398
399fn tensor_to_json_string(tensor: &Tensor) -> String {
402 match tensor {
403 Tensor::Scalar(n) => {
404 if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {
405 format!("{}", *n as i64)
407 } else {
408 format!("{n}")
409 }
410 }
411 Tensor::Array(items) => {
412 let inner: Vec<String> = items.iter().map(tensor_to_json_string).collect();
413 format!("[{}]", inner.join(","))
414 }
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use hedl_core::lex::{Expression, Span};
422 use hedl_core::{Document, Item, MatrixList, Node, Reference, Value};
423
424 fn create_test_document() -> Document {
425 let mut doc = Document::new((1, 0));
426
427 let mut list = MatrixList::new(
430 "Person",
431 vec![
432 "id".to_string(),
433 "name".to_string(),
434 "age".to_string(),
435 "active".to_string(),
436 ],
437 );
438
439 list.add_row(Node::new(
440 "Person",
441 "1",
442 vec![
443 Value::String("1".into()),
444 Value::String("Alice".into()),
445 Value::Int(30),
446 Value::Bool(true),
447 ],
448 ));
449
450 list.add_row(Node::new(
451 "Person",
452 "2",
453 vec![
454 Value::String("2".into()),
455 Value::String("Bob".into()),
456 Value::Int(25),
457 Value::Bool(false),
458 ],
459 ));
460
461 doc.root.insert("people".to_string(), Item::List(list));
462 doc
463 }
464
465 #[test]
468 fn test_to_csv_config_default() {
469 let config = ToCsvConfig::default();
470 assert_eq!(config.delimiter, b',');
471 assert!(config.include_headers);
472 assert!(matches!(config.quote_style, csv::QuoteStyle::Necessary));
473 }
474
475 #[test]
476 fn test_to_csv_config_debug() {
477 let config = ToCsvConfig::default();
478 let debug = format!("{config:?}");
479 assert!(debug.contains("ToCsvConfig"));
480 assert!(debug.contains("delimiter"));
481 assert!(debug.contains("include_headers"));
482 assert!(debug.contains("quote_style"));
483 }
484
485 #[test]
486 fn test_to_csv_config_clone() {
487 let config = ToCsvConfig {
488 delimiter: b'\t',
489 include_headers: false,
490 quote_style: csv::QuoteStyle::Always,
491 };
492 let cloned = config.clone();
493 assert_eq!(cloned.delimiter, b'\t');
494 assert!(!cloned.include_headers);
495 }
496
497 #[test]
498 fn test_to_csv_config_all_options() {
499 let config = ToCsvConfig {
500 delimiter: b';',
501 include_headers: true,
502 quote_style: csv::QuoteStyle::Always,
503 };
504 assert_eq!(config.delimiter, b';');
505 assert!(config.include_headers);
506 }
507
508 #[test]
511 fn test_to_csv_basic() {
512 let doc = create_test_document();
513 let csv = to_csv(&doc).unwrap();
514
515 let expected = "id,name,age,active\n1,Alice,30,true\n2,Bob,25,false\n";
516 assert_eq!(csv, expected);
517 }
518
519 #[test]
520 fn test_to_csv_without_headers() {
521 let doc = create_test_document();
522 let config = ToCsvConfig {
523 include_headers: false,
524 ..Default::default()
525 };
526 let csv = to_csv_with_config(&doc, config).unwrap();
527
528 let expected = "1,Alice,30,true\n2,Bob,25,false\n";
529 assert_eq!(csv, expected);
530 }
531
532 #[test]
533 fn test_to_csv_custom_delimiter() {
534 let doc = create_test_document();
535 let config = ToCsvConfig {
536 delimiter: b'\t',
537 ..Default::default()
538 };
539 let csv = to_csv_with_config(&doc, config).unwrap();
540
541 let expected = "id\tname\tage\tactive\n1\tAlice\t30\ttrue\n2\tBob\t25\tfalse\n";
542 assert_eq!(csv, expected);
543 }
544
545 #[test]
546 fn test_to_csv_semicolon_delimiter() {
547 let doc = create_test_document();
548 let config = ToCsvConfig {
549 delimiter: b';',
550 ..Default::default()
551 };
552 let csv = to_csv_with_config(&doc, config).unwrap();
553
554 assert!(csv.contains(';'));
555 assert!(csv.contains("Alice"));
556 }
557
558 #[test]
559 fn test_to_csv_empty_list() {
560 let mut doc = Document::new((1, 0));
561 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
562 doc.root.insert("people".to_string(), Item::List(list));
563
564 let csv = to_csv(&doc).unwrap();
565 assert_eq!(csv, "id,name\n");
566 }
567
568 #[test]
569 fn test_to_csv_empty_list_no_headers() {
570 let mut doc = Document::new((1, 0));
571 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
572 doc.root.insert("people".to_string(), Item::List(list));
573
574 let config = ToCsvConfig {
575 include_headers: false,
576 ..Default::default()
577 };
578 let csv = to_csv_with_config(&doc, config).unwrap();
579 assert!(csv.is_empty());
580 }
581
582 #[test]
585 fn test_value_to_csv_string_null() {
586 assert_eq!(value_to_csv_string(&Value::Null), "");
587 }
588
589 #[test]
590 fn test_value_to_csv_string_bool_true() {
591 assert_eq!(value_to_csv_string(&Value::Bool(true)), "true");
592 }
593
594 #[test]
595 fn test_value_to_csv_string_bool_false() {
596 assert_eq!(value_to_csv_string(&Value::Bool(false)), "false");
597 }
598
599 #[test]
600 fn test_value_to_csv_string_int_positive() {
601 assert_eq!(value_to_csv_string(&Value::Int(42)), "42");
602 }
603
604 #[test]
605 fn test_value_to_csv_string_int_negative() {
606 assert_eq!(value_to_csv_string(&Value::Int(-100)), "-100");
607 }
608
609 #[test]
610 fn test_value_to_csv_string_int_zero() {
611 assert_eq!(value_to_csv_string(&Value::Int(0)), "0");
612 }
613
614 #[test]
615 fn test_value_to_csv_string_int_large() {
616 assert_eq!(
617 value_to_csv_string(&Value::Int(i64::MAX)),
618 i64::MAX.to_string()
619 );
620 }
621
622 #[test]
623 fn test_value_to_csv_string_float_positive() {
624 assert_eq!(value_to_csv_string(&Value::Float(3.25)), "3.25");
625 }
626
627 #[test]
628 fn test_value_to_csv_string_float_negative() {
629 assert_eq!(value_to_csv_string(&Value::Float(-2.5)), "-2.5");
630 }
631
632 #[test]
633 fn test_value_to_csv_string_float_zero() {
634 assert_eq!(value_to_csv_string(&Value::Float(0.0)), "0");
635 }
636
637 #[test]
638 fn test_value_to_csv_string_string() {
639 assert_eq!(value_to_csv_string(&Value::String("hello".into())), "hello");
640 }
641
642 #[test]
643 fn test_value_to_csv_string_string_empty() {
644 assert_eq!(value_to_csv_string(&Value::String("".into())), "");
645 }
646
647 #[test]
648 fn test_value_to_csv_string_string_with_comma() {
649 assert_eq!(
651 value_to_csv_string(&Value::String("hello, world".into())),
652 "hello, world"
653 );
654 }
655
656 #[test]
657 fn test_value_to_csv_string_reference_local() {
658 assert_eq!(
659 value_to_csv_string(&Value::Reference(Reference::local("user1"))),
660 "@user1"
661 );
662 }
663
664 #[test]
665 fn test_value_to_csv_string_reference_qualified() {
666 assert_eq!(
667 value_to_csv_string(&Value::Reference(Reference::qualified("User", "123"))),
668 "@User:123"
669 );
670 }
671
672 #[test]
673 fn test_value_to_csv_string_expression_identifier() {
674 let expr = Value::Expression(Box::new(Expression::Identifier {
675 name: "foo".to_string(),
676 span: Span::synthetic(),
677 }));
678 assert_eq!(value_to_csv_string(&expr), "$(foo)");
679 }
680
681 #[test]
682 fn test_value_to_csv_string_expression_call() {
683 let expr = Value::Expression(Box::new(Expression::Call {
684 name: "add".to_string(),
685 args: vec![
686 Expression::Identifier {
687 name: "x".to_string(),
688 span: Span::synthetic(),
689 },
690 Expression::Literal {
691 value: hedl_core::lex::ExprLiteral::Int(1),
692 span: Span::synthetic(),
693 },
694 ],
695 span: Span::synthetic(),
696 }));
697 assert_eq!(value_to_csv_string(&expr), "$(add(x, 1))");
698 }
699
700 #[test]
703 fn test_special_float_nan() {
704 assert_eq!(value_to_csv_string(&Value::Float(f64::NAN)), "NaN");
705 }
706
707 #[test]
708 fn test_special_float_infinity() {
709 assert_eq!(
710 value_to_csv_string(&Value::Float(f64::INFINITY)),
711 "Infinity"
712 );
713 }
714
715 #[test]
716 fn test_special_float_neg_infinity() {
717 assert_eq!(
718 value_to_csv_string(&Value::Float(f64::NEG_INFINITY)),
719 "-Infinity"
720 );
721 }
722
723 #[test]
726 fn test_tensor_scalar_int() {
727 let tensor = Tensor::Scalar(42.0);
728 assert_eq!(tensor_to_json_string(&tensor), "42");
729 }
730
731 #[test]
732 fn test_tensor_scalar_float() {
733 let tensor = Tensor::Scalar(3.5);
734 assert_eq!(tensor_to_json_string(&tensor), "3.5");
735 }
736
737 #[test]
738 fn test_tensor_1d_array() {
739 let tensor = Tensor::Array(vec![
740 Tensor::Scalar(1.0),
741 Tensor::Scalar(2.0),
742 Tensor::Scalar(3.0),
743 ]);
744 assert_eq!(tensor_to_json_string(&tensor), "[1,2,3]");
745 }
746
747 #[test]
748 fn test_tensor_2d_array() {
749 let tensor = Tensor::Array(vec![
750 Tensor::Array(vec![Tensor::Scalar(1.0), Tensor::Scalar(2.0)]),
751 Tensor::Array(vec![Tensor::Scalar(3.0), Tensor::Scalar(4.0)]),
752 ]);
753 assert_eq!(tensor_to_json_string(&tensor), "[[1,2],[3,4]]");
754 }
755
756 #[test]
757 fn test_tensor_empty_array() {
758 let tensor = Tensor::Array(vec![]);
759 assert_eq!(tensor_to_json_string(&tensor), "[]");
760 }
761
762 #[test]
763 fn test_value_to_csv_string_tensor() {
764 let tensor = Tensor::Array(vec![Tensor::Scalar(1.0), Tensor::Scalar(2.0)]);
765 assert_eq!(
766 value_to_csv_string(&Value::Tensor(Box::new(tensor))),
767 "[1,2]"
768 );
769 }
770
771 #[test]
774 fn test_no_matrix_list_error() {
775 let doc = Document::new((1, 0));
776 let result = to_csv(&doc);
777
778 assert!(result.is_err());
779 let err = result.unwrap_err();
780 assert!(matches!(
781 err,
782 CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }
783 ));
784 }
785
786 #[test]
787 fn test_no_matrix_list_with_scalar() {
788 let mut doc = Document::new((1, 0));
789 doc.root
790 .insert("value".to_string(), Item::Scalar(Value::Int(42)));
791
792 let result = to_csv(&doc);
793 assert!(result.is_err());
794 assert!(matches!(
795 result.unwrap_err(),
796 CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }
797 ));
798 }
799
800 #[test]
803 fn test_to_csv_writer_basic() {
804 let doc = create_test_document();
805 let mut buffer = Vec::new();
806 to_csv_writer(&doc, &mut buffer).unwrap();
807
808 let csv = String::from_utf8(buffer).unwrap();
809 assert!(csv.contains("Alice"));
810 assert!(csv.contains("Bob"));
811 }
812
813 #[test]
814 fn test_to_csv_writer_with_config() {
815 let doc = create_test_document();
816 let config = ToCsvConfig {
817 include_headers: false,
818 ..Default::default()
819 };
820 let mut buffer = Vec::new();
821 to_csv_writer_with_config(&doc, &mut buffer, config).unwrap();
822
823 let csv = String::from_utf8(buffer).unwrap();
824 assert!(!csv.contains("id,name"));
825 assert!(csv.contains("Alice"));
826 }
827
828 #[test]
831 fn test_quoting_with_comma() {
832 let mut doc = Document::new((1, 0));
833 let mut list = MatrixList::new("Item", vec!["id".to_string(), "text".to_string()]);
834 list.add_row(Node::new(
835 "Item",
836 "1",
837 vec![
838 Value::String("1".into()),
839 Value::String("hello, world".into()),
840 ],
841 ));
842 doc.root.insert("items".to_string(), Item::List(list));
843
844 let csv = to_csv(&doc).unwrap();
845 assert!(csv.contains("\"hello, world\""));
847 }
848
849 #[test]
850 fn test_quoting_with_newline() {
851 let mut doc = Document::new((1, 0));
852 let mut list = MatrixList::new("Item", vec!["id".to_string(), "text".to_string()]);
853 list.add_row(Node::new(
854 "Item",
855 "1",
856 vec![
857 Value::String("1".into()),
858 Value::String("line1\nline2".into()),
859 ],
860 ));
861 doc.root.insert("items".to_string(), Item::List(list));
862
863 let csv = to_csv(&doc).unwrap();
864 assert!(csv.contains("\"line1\nline2\""));
866 }
867
868 #[test]
871 fn test_to_csv_list_basic() {
872 let mut doc = Document::new((1, 0));
873 let mut list = MatrixList::new(
874 "Person",
875 vec![
876 "id".to_string(),
877 "name".to_string(),
878 "age".to_string(),
879 "active".to_string(),
880 ],
881 );
882
883 list.add_row(Node::new(
884 "Person",
885 "1",
886 vec![
887 Value::String("1".into()),
888 Value::String("Alice".into()),
889 Value::Int(30),
890 Value::Bool(true),
891 ],
892 ));
893
894 list.add_row(Node::new(
895 "Person",
896 "2",
897 vec![
898 Value::String("2".into()),
899 Value::String("Bob".into()),
900 Value::Int(25),
901 Value::Bool(false),
902 ],
903 ));
904
905 doc.root.insert("people".to_string(), Item::List(list));
906
907 let csv = to_csv_list(&doc, "people").unwrap();
908 let expected = "id,name,age,active\n1,Alice,30,true\n2,Bob,25,false\n";
909 assert_eq!(csv, expected);
910 }
911
912 #[test]
913 fn test_to_csv_list_selective_export() {
914 let mut doc = Document::new((1, 0));
915
916 let mut people_list = MatrixList::new(
918 "Person",
919 vec!["id".to_string(), "name".to_string(), "age".to_string()],
920 );
921 people_list.add_row(Node::new(
922 "Person",
923 "1",
924 vec![
925 Value::String("1".into()),
926 Value::String("Alice".into()),
927 Value::Int(30),
928 ],
929 ));
930 doc.root
931 .insert("people".to_string(), Item::List(people_list));
932
933 let mut items_list = MatrixList::new(
935 "Item",
936 vec!["id".to_string(), "name".to_string(), "price".to_string()],
937 );
938 items_list.add_row(Node::new(
939 "Item",
940 "101",
941 vec![
942 Value::String("101".into()),
943 Value::String("Widget".into()),
944 Value::Float(9.99),
945 ],
946 ));
947 doc.root.insert("items".to_string(), Item::List(items_list));
948
949 let csv_people = to_csv_list(&doc, "people").unwrap();
951 assert!(csv_people.contains("Alice"));
952 assert!(!csv_people.contains("Widget"));
953
954 let csv_items = to_csv_list(&doc, "items").unwrap();
956 assert!(csv_items.contains("Widget"));
957 assert!(!csv_items.contains("Alice"));
958 }
959
960 #[test]
961 fn test_to_csv_list_not_found() {
962 let doc = Document::new((1, 0));
963 let result = to_csv_list(&doc, "nonexistent");
964
965 assert!(result.is_err());
966 let err = result.unwrap_err();
967 assert!(matches!(
968 err,
969 CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }
970 ));
971 assert!(err.to_string().contains("not found"));
972 }
973
974 #[test]
975 fn test_to_csv_list_not_a_list() {
976 let mut doc = Document::new((1, 0));
977 doc.root
978 .insert("scalar".to_string(), Item::Scalar(Value::Int(42)));
979
980 let result = to_csv_list(&doc, "scalar");
981 assert!(result.is_err());
982 let err = result.unwrap_err();
983 assert!(matches!(
984 err,
985 CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }
986 ));
987 assert!(err.to_string().contains("not a matrix list"));
988 }
989
990 #[test]
991 fn test_to_csv_list_without_headers() {
992 let mut doc = Document::new((1, 0));
993 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
994
995 list.add_row(Node::new(
996 "Person",
997 "1",
998 vec![Value::String("1".into()), Value::String("Alice".into())],
999 ));
1000
1001 doc.root.insert("people".to_string(), Item::List(list));
1002
1003 let config = ToCsvConfig {
1004 include_headers: false,
1005 ..Default::default()
1006 };
1007 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
1008
1009 let expected = "1,Alice\n";
1010 assert_eq!(csv, expected);
1011 }
1012
1013 #[test]
1014 fn test_to_csv_list_custom_delimiter() {
1015 let mut doc = Document::new((1, 0));
1016 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1017
1018 list.add_row(Node::new(
1019 "Person",
1020 "1",
1021 vec![Value::String("1".into()), Value::String("Alice".into())],
1022 ));
1023
1024 doc.root.insert("people".to_string(), Item::List(list));
1025
1026 let config = ToCsvConfig {
1027 delimiter: b';',
1028 ..Default::default()
1029 };
1030 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
1031
1032 let expected = "id;name\n1;Alice\n";
1033 assert_eq!(csv, expected);
1034 }
1035
1036 #[test]
1037 fn test_to_csv_list_tab_delimiter() {
1038 let mut doc = Document::new((1, 0));
1039 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1040
1041 list.add_row(Node::new(
1042 "Person",
1043 "1",
1044 vec![Value::String("1".into()), Value::String("Alice".into())],
1045 ));
1046
1047 doc.root.insert("people".to_string(), Item::List(list));
1048
1049 let config = ToCsvConfig {
1050 delimiter: b'\t',
1051 ..Default::default()
1052 };
1053 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
1054
1055 assert!(csv.contains("id\tname"));
1056 assert!(csv.contains("1\tAlice"));
1057 }
1058
1059 #[test]
1060 fn test_to_csv_list_empty() {
1061 let mut doc = Document::new((1, 0));
1062 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1063 doc.root.insert("people".to_string(), Item::List(list));
1064
1065 let csv = to_csv_list(&doc, "people").unwrap();
1066 let expected = "id,name\n";
1067 assert_eq!(csv, expected);
1068 }
1069
1070 #[test]
1071 fn test_to_csv_list_empty_no_headers() {
1072 let mut doc = Document::new((1, 0));
1073 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1074 doc.root.insert("people".to_string(), Item::List(list));
1075
1076 let config = ToCsvConfig {
1077 include_headers: false,
1078 ..Default::default()
1079 };
1080 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
1081 assert!(csv.is_empty());
1082 }
1083
1084 #[test]
1085 fn test_to_csv_list_writer() {
1086 let mut doc = Document::new((1, 0));
1087 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1088
1089 list.add_row(Node::new(
1090 "Person",
1091 "1",
1092 vec![Value::String("1".into()), Value::String("Alice".into())],
1093 ));
1094
1095 doc.root.insert("people".to_string(), Item::List(list));
1096
1097 let mut buffer = Vec::new();
1098 to_csv_list_writer(&doc, "people", &mut buffer).unwrap();
1099
1100 let csv = String::from_utf8(buffer).unwrap();
1101 assert!(csv.contains("Alice"));
1102 }
1103
1104 #[test]
1105 fn test_to_csv_list_writer_with_config() {
1106 let mut doc = Document::new((1, 0));
1107 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1108
1109 list.add_row(Node::new(
1110 "Person",
1111 "1",
1112 vec![Value::String("1".into()), Value::String("Alice".into())],
1113 ));
1114
1115 doc.root.insert("people".to_string(), Item::List(list));
1116
1117 let config = ToCsvConfig {
1118 include_headers: false,
1119 ..Default::default()
1120 };
1121 let mut buffer = Vec::new();
1122 to_csv_list_writer_with_config(&doc, "people", &mut buffer, config).unwrap();
1123
1124 let csv = String::from_utf8(buffer).unwrap();
1125 assert_eq!(csv, "1,Alice\n");
1126 }
1127
1128 #[test]
1129 fn test_to_csv_list_with_all_value_types() {
1130 let mut doc = Document::new((1, 0));
1131 let mut list = MatrixList::new(
1132 "Data",
1133 vec![
1134 "id".to_string(),
1135 "bool_val".to_string(),
1136 "int_val".to_string(),
1137 "float_val".to_string(),
1138 "string_val".to_string(),
1139 "null_val".to_string(),
1140 "ref_val".to_string(),
1141 ],
1142 );
1143
1144 list.add_row(Node::new(
1145 "Data",
1146 "1",
1147 vec![
1148 Value::String("1".into()),
1149 Value::Bool(true),
1150 Value::Int(42),
1151 Value::Float(3.5),
1152 Value::String("hello".into()),
1153 Value::Null,
1154 Value::Reference(Reference::local("user1")),
1155 ],
1156 ));
1157
1158 doc.root.insert("data".to_string(), Item::List(list));
1159
1160 let csv = to_csv_list(&doc, "data").unwrap();
1161 assert!(csv.contains("true"));
1162 assert!(csv.contains("42"));
1163 assert!(csv.contains("3.5"));
1164 assert!(csv.contains("hello"));
1165 assert!(csv.contains("@user1"));
1166 }
1167
1168 #[test]
1169 fn test_to_csv_list_with_nested_children_skipped() {
1170 let mut doc = Document::new((1, 0));
1171 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1172
1173 let mut person = Node::new(
1174 "Person",
1175 "1",
1176 vec![Value::String("1".into()), Value::String("Alice".into())],
1177 );
1178
1179 let child = Node::new(
1181 "Address",
1182 "addr1",
1183 vec![
1184 Value::String("addr1".into()),
1185 Value::String("123 Main St".into()),
1186 ],
1187 );
1188 person.add_child("Address", child);
1189
1190 list.add_row(person);
1191 doc.root.insert("people".to_string(), Item::List(list));
1192
1193 let csv = to_csv_list(&doc, "people").unwrap();
1195 assert!(csv.contains("Alice"));
1196 assert!(!csv.contains("Address"));
1197 assert!(!csv.contains("123 Main St"));
1198 }
1199
1200 #[test]
1201 fn test_to_csv_list_complex_quoting() {
1202 let mut doc = Document::new((1, 0));
1203 let mut list = MatrixList::new("Item", vec!["id".to_string(), "description".to_string()]);
1204
1205 list.add_row(Node::new(
1206 "Item",
1207 "1",
1208 vec![
1209 Value::String("1".into()),
1210 Value::String("Contains, comma and \"quotes\"".into()),
1211 ],
1212 ));
1213
1214 doc.root.insert("items".to_string(), Item::List(list));
1215
1216 let csv = to_csv_list(&doc, "items").unwrap();
1217 assert!(csv.contains("comma"));
1219 }
1220
1221 #[test]
1222 fn test_to_csv_list_multiple_lists_independent() {
1223 let mut doc = Document::new((1, 0));
1224
1225 let mut list1 = MatrixList::new("Type1", vec!["id".to_string(), "val".to_string()]);
1227 list1.add_row(Node::new(
1228 "Type1",
1229 "1",
1230 vec![Value::String("1".into()), Value::String("alpha".into())],
1231 ));
1232 list1.add_row(Node::new(
1233 "Type1",
1234 "2",
1235 vec![Value::String("2".into()), Value::String("bravo".into())],
1236 ));
1237 doc.root.insert("list1".to_string(), Item::List(list1));
1238
1239 let mut list2 = MatrixList::new("Type2", vec!["id".to_string(), "val".to_string()]);
1241 list2.add_row(Node::new(
1242 "Type2",
1243 "1",
1244 vec![Value::String("1".into()), Value::String("x_ray".into())],
1245 ));
1246 list2.add_row(Node::new(
1247 "Type2",
1248 "2",
1249 vec![Value::String("2".into()), Value::String("yankee".into())],
1250 ));
1251 list2.add_row(Node::new(
1252 "Type2",
1253 "3",
1254 vec![Value::String("3".into()), Value::String("zulu".into())],
1255 ));
1256 doc.root.insert("list2".to_string(), Item::List(list2));
1257
1258 let csv1 = to_csv_list(&doc, "list1").unwrap();
1260 let csv2 = to_csv_list(&doc, "list2").unwrap();
1261
1262 let lines1: Vec<&str> = csv1.lines().collect();
1264 assert_eq!(lines1.len(), 3); let lines2: Vec<&str> = csv2.lines().collect();
1268 assert_eq!(lines2.len(), 4); assert!(csv1.contains("alpha") && csv1.contains("bravo"));
1272 assert!(csv2.contains("x_ray") && csv2.contains("yankee") && csv2.contains("zulu"));
1273 assert!(!csv1.contains("x_ray"));
1274 assert!(!csv2.contains("alpha"));
1275 }
1276
1277 #[test]
1278 fn test_to_csv_list_special_floats() {
1279 let mut doc = Document::new((1, 0));
1280 let mut list = MatrixList::new("Data", vec!["id".to_string(), "value".to_string()]);
1281
1282 list.add_row(Node::new(
1283 "Data",
1284 "1",
1285 vec![Value::String("1".into()), Value::Float(f64::NAN)],
1286 ));
1287
1288 list.add_row(Node::new(
1289 "Data",
1290 "2",
1291 vec![Value::String("2".into()), Value::Float(f64::INFINITY)],
1292 ));
1293
1294 list.add_row(Node::new(
1295 "Data",
1296 "3",
1297 vec![Value::String("3".into()), Value::Float(f64::NEG_INFINITY)],
1298 ));
1299
1300 doc.root.insert("data".to_string(), Item::List(list));
1301
1302 let csv = to_csv_list(&doc, "data").unwrap();
1303 assert!(csv.contains("NaN"));
1304 assert!(csv.contains("Infinity"));
1305 assert!(csv.contains("-Infinity"));
1306 }
1307}