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 Value::List(items) => list_to_csv_string(items),
397 }
398}
399
400fn tensor_to_json_string(tensor: &Tensor) -> String {
403 match tensor {
404 Tensor::Scalar(n) => {
405 if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {
406 format!("{}", *n as i64)
408 } else {
409 format!("{n}")
410 }
411 }
412 Tensor::Array(items) => {
413 let inner: Vec<String> = items.iter().map(tensor_to_json_string).collect();
414 format!("[{}]", inner.join(","))
415 }
416 }
417}
418
419fn list_to_csv_string(items: &[Value]) -> String {
422 if items.is_empty() {
423 return "()".to_string();
424 }
425
426 let inner: Vec<String> = items
427 .iter()
428 .map(|item| match item {
429 Value::String(s) => s.to_string(),
430 other => value_to_csv_string(other),
431 })
432 .collect();
433
434 format!("({})", inner.join(", "))
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use hedl_core::lex::{Expression, Span};
441 use hedl_core::{Document, Item, MatrixList, Node, Reference, Value};
442
443 fn create_test_document() -> Document {
444 let mut doc = Document::new((1, 0));
445
446 let mut list = MatrixList::new(
449 "Person",
450 vec![
451 "id".to_string(),
452 "name".to_string(),
453 "age".to_string(),
454 "active".to_string(),
455 ],
456 );
457
458 list.add_row(Node::new(
459 "Person",
460 "1",
461 vec![
462 Value::String("1".into()),
463 Value::String("Alice".into()),
464 Value::Int(30),
465 Value::Bool(true),
466 ],
467 ));
468
469 list.add_row(Node::new(
470 "Person",
471 "2",
472 vec![
473 Value::String("2".into()),
474 Value::String("Bob".into()),
475 Value::Int(25),
476 Value::Bool(false),
477 ],
478 ));
479
480 doc.root.insert("people".to_string(), Item::List(list));
481 doc
482 }
483
484 #[test]
487 fn test_to_csv_config_default() {
488 let config = ToCsvConfig::default();
489 assert_eq!(config.delimiter, b',');
490 assert!(config.include_headers);
491 assert!(matches!(config.quote_style, csv::QuoteStyle::Necessary));
492 }
493
494 #[test]
495 fn test_to_csv_config_debug() {
496 let config = ToCsvConfig::default();
497 let debug = format!("{config:?}");
498 assert!(debug.contains("ToCsvConfig"));
499 assert!(debug.contains("delimiter"));
500 assert!(debug.contains("include_headers"));
501 assert!(debug.contains("quote_style"));
502 }
503
504 #[test]
505 fn test_to_csv_config_clone() {
506 let config = ToCsvConfig {
507 delimiter: b'\t',
508 include_headers: false,
509 quote_style: csv::QuoteStyle::Always,
510 };
511 let cloned = config.clone();
512 assert_eq!(cloned.delimiter, b'\t');
513 assert!(!cloned.include_headers);
514 }
515
516 #[test]
517 fn test_to_csv_config_all_options() {
518 let config = ToCsvConfig {
519 delimiter: b';',
520 include_headers: true,
521 quote_style: csv::QuoteStyle::Always,
522 };
523 assert_eq!(config.delimiter, b';');
524 assert!(config.include_headers);
525 }
526
527 #[test]
530 fn test_to_csv_basic() {
531 let doc = create_test_document();
532 let csv = to_csv(&doc).unwrap();
533
534 let expected = "id,name,age,active\n1,Alice,30,true\n2,Bob,25,false\n";
535 assert_eq!(csv, expected);
536 }
537
538 #[test]
539 fn test_to_csv_without_headers() {
540 let doc = create_test_document();
541 let config = ToCsvConfig {
542 include_headers: false,
543 ..Default::default()
544 };
545 let csv = to_csv_with_config(&doc, config).unwrap();
546
547 let expected = "1,Alice,30,true\n2,Bob,25,false\n";
548 assert_eq!(csv, expected);
549 }
550
551 #[test]
552 fn test_to_csv_custom_delimiter() {
553 let doc = create_test_document();
554 let config = ToCsvConfig {
555 delimiter: b'\t',
556 ..Default::default()
557 };
558 let csv = to_csv_with_config(&doc, config).unwrap();
559
560 let expected = "id\tname\tage\tactive\n1\tAlice\t30\ttrue\n2\tBob\t25\tfalse\n";
561 assert_eq!(csv, expected);
562 }
563
564 #[test]
565 fn test_to_csv_semicolon_delimiter() {
566 let doc = create_test_document();
567 let config = ToCsvConfig {
568 delimiter: b';',
569 ..Default::default()
570 };
571 let csv = to_csv_with_config(&doc, config).unwrap();
572
573 assert!(csv.contains(';'));
574 assert!(csv.contains("Alice"));
575 }
576
577 #[test]
578 fn test_to_csv_empty_list() {
579 let mut doc = Document::new((1, 0));
580 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
581 doc.root.insert("people".to_string(), Item::List(list));
582
583 let csv = to_csv(&doc).unwrap();
584 assert_eq!(csv, "id,name\n");
585 }
586
587 #[test]
588 fn test_to_csv_empty_list_no_headers() {
589 let mut doc = Document::new((1, 0));
590 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
591 doc.root.insert("people".to_string(), Item::List(list));
592
593 let config = ToCsvConfig {
594 include_headers: false,
595 ..Default::default()
596 };
597 let csv = to_csv_with_config(&doc, config).unwrap();
598 assert!(csv.is_empty());
599 }
600
601 #[test]
604 fn test_value_to_csv_string_null() {
605 assert_eq!(value_to_csv_string(&Value::Null), "");
606 }
607
608 #[test]
609 fn test_value_to_csv_string_bool_true() {
610 assert_eq!(value_to_csv_string(&Value::Bool(true)), "true");
611 }
612
613 #[test]
614 fn test_value_to_csv_string_bool_false() {
615 assert_eq!(value_to_csv_string(&Value::Bool(false)), "false");
616 }
617
618 #[test]
619 fn test_value_to_csv_string_int_positive() {
620 assert_eq!(value_to_csv_string(&Value::Int(42)), "42");
621 }
622
623 #[test]
624 fn test_value_to_csv_string_int_negative() {
625 assert_eq!(value_to_csv_string(&Value::Int(-100)), "-100");
626 }
627
628 #[test]
629 fn test_value_to_csv_string_int_zero() {
630 assert_eq!(value_to_csv_string(&Value::Int(0)), "0");
631 }
632
633 #[test]
634 fn test_value_to_csv_string_int_large() {
635 assert_eq!(
636 value_to_csv_string(&Value::Int(i64::MAX)),
637 i64::MAX.to_string()
638 );
639 }
640
641 #[test]
642 fn test_value_to_csv_string_float_positive() {
643 assert_eq!(value_to_csv_string(&Value::Float(3.25)), "3.25");
644 }
645
646 #[test]
647 fn test_value_to_csv_string_float_negative() {
648 assert_eq!(value_to_csv_string(&Value::Float(-2.5)), "-2.5");
649 }
650
651 #[test]
652 fn test_value_to_csv_string_float_zero() {
653 assert_eq!(value_to_csv_string(&Value::Float(0.0)), "0");
654 }
655
656 #[test]
657 fn test_value_to_csv_string_string() {
658 assert_eq!(value_to_csv_string(&Value::String("hello".into())), "hello");
659 }
660
661 #[test]
662 fn test_value_to_csv_string_string_empty() {
663 assert_eq!(value_to_csv_string(&Value::String("".into())), "");
664 }
665
666 #[test]
667 fn test_value_to_csv_string_string_with_comma() {
668 assert_eq!(
670 value_to_csv_string(&Value::String("hello, world".into())),
671 "hello, world"
672 );
673 }
674
675 #[test]
676 fn test_value_to_csv_string_reference_local() {
677 assert_eq!(
678 value_to_csv_string(&Value::Reference(Reference::local("user1"))),
679 "@user1"
680 );
681 }
682
683 #[test]
684 fn test_value_to_csv_string_reference_qualified() {
685 assert_eq!(
686 value_to_csv_string(&Value::Reference(Reference::qualified("User", "123"))),
687 "@User:123"
688 );
689 }
690
691 #[test]
692 fn test_value_to_csv_string_expression_identifier() {
693 let expr = Value::Expression(Box::new(Expression::Identifier {
694 name: "foo".to_string(),
695 span: Span::synthetic(),
696 }));
697 assert_eq!(value_to_csv_string(&expr), "$(foo)");
698 }
699
700 #[test]
701 fn test_value_to_csv_string_expression_call() {
702 let expr = Value::Expression(Box::new(Expression::Call {
703 name: "add".to_string(),
704 args: vec![
705 Expression::Identifier {
706 name: "x".to_string(),
707 span: Span::synthetic(),
708 },
709 Expression::Literal {
710 value: hedl_core::lex::ExprLiteral::Int(1),
711 span: Span::synthetic(),
712 },
713 ],
714 span: Span::synthetic(),
715 }));
716 assert_eq!(value_to_csv_string(&expr), "$(add(x, 1))");
717 }
718
719 #[test]
722 fn test_special_float_nan() {
723 assert_eq!(value_to_csv_string(&Value::Float(f64::NAN)), "NaN");
724 }
725
726 #[test]
727 fn test_special_float_infinity() {
728 assert_eq!(
729 value_to_csv_string(&Value::Float(f64::INFINITY)),
730 "Infinity"
731 );
732 }
733
734 #[test]
735 fn test_special_float_neg_infinity() {
736 assert_eq!(
737 value_to_csv_string(&Value::Float(f64::NEG_INFINITY)),
738 "-Infinity"
739 );
740 }
741
742 #[test]
745 fn test_tensor_scalar_int() {
746 let tensor = Tensor::Scalar(42.0);
747 assert_eq!(tensor_to_json_string(&tensor), "42");
748 }
749
750 #[test]
751 fn test_tensor_scalar_float() {
752 let tensor = Tensor::Scalar(3.5);
753 assert_eq!(tensor_to_json_string(&tensor), "3.5");
754 }
755
756 #[test]
757 fn test_tensor_1d_array() {
758 let tensor = Tensor::Array(vec![
759 Tensor::Scalar(1.0),
760 Tensor::Scalar(2.0),
761 Tensor::Scalar(3.0),
762 ]);
763 assert_eq!(tensor_to_json_string(&tensor), "[1,2,3]");
764 }
765
766 #[test]
767 fn test_tensor_2d_array() {
768 let tensor = Tensor::Array(vec![
769 Tensor::Array(vec![Tensor::Scalar(1.0), Tensor::Scalar(2.0)]),
770 Tensor::Array(vec![Tensor::Scalar(3.0), Tensor::Scalar(4.0)]),
771 ]);
772 assert_eq!(tensor_to_json_string(&tensor), "[[1,2],[3,4]]");
773 }
774
775 #[test]
776 fn test_tensor_empty_array() {
777 let tensor = Tensor::Array(vec![]);
778 assert_eq!(tensor_to_json_string(&tensor), "[]");
779 }
780
781 #[test]
782 fn test_value_to_csv_string_tensor() {
783 let tensor = Tensor::Array(vec![Tensor::Scalar(1.0), Tensor::Scalar(2.0)]);
784 assert_eq!(
785 value_to_csv_string(&Value::Tensor(Box::new(tensor))),
786 "[1,2]"
787 );
788 }
789
790 #[test]
793 fn test_no_matrix_list_error() {
794 let doc = Document::new((1, 0));
795 let result = to_csv(&doc);
796
797 assert!(result.is_err());
798 let err = result.unwrap_err();
799 assert!(matches!(
800 err,
801 CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }
802 ));
803 }
804
805 #[test]
806 fn test_no_matrix_list_with_scalar() {
807 let mut doc = Document::new((1, 0));
808 doc.root
809 .insert("value".to_string(), Item::Scalar(Value::Int(42)));
810
811 let result = to_csv(&doc);
812 assert!(result.is_err());
813 assert!(matches!(
814 result.unwrap_err(),
815 CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }
816 ));
817 }
818
819 #[test]
822 fn test_to_csv_writer_basic() {
823 let doc = create_test_document();
824 let mut buffer = Vec::new();
825 to_csv_writer(&doc, &mut buffer).unwrap();
826
827 let csv = String::from_utf8(buffer).unwrap();
828 assert!(csv.contains("Alice"));
829 assert!(csv.contains("Bob"));
830 }
831
832 #[test]
833 fn test_to_csv_writer_with_config() {
834 let doc = create_test_document();
835 let config = ToCsvConfig {
836 include_headers: false,
837 ..Default::default()
838 };
839 let mut buffer = Vec::new();
840 to_csv_writer_with_config(&doc, &mut buffer, config).unwrap();
841
842 let csv = String::from_utf8(buffer).unwrap();
843 assert!(!csv.contains("id,name"));
844 assert!(csv.contains("Alice"));
845 }
846
847 #[test]
850 fn test_quoting_with_comma() {
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("hello, world".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("\"hello, world\""));
866 }
867
868 #[test]
869 fn test_quoting_with_newline() {
870 let mut doc = Document::new((1, 0));
871 let mut list = MatrixList::new("Item", vec!["id".to_string(), "text".to_string()]);
872 list.add_row(Node::new(
873 "Item",
874 "1",
875 vec![
876 Value::String("1".into()),
877 Value::String("line1\nline2".into()),
878 ],
879 ));
880 doc.root.insert("items".to_string(), Item::List(list));
881
882 let csv = to_csv(&doc).unwrap();
883 assert!(csv.contains("\"line1\nline2\""));
885 }
886
887 #[test]
890 fn test_to_csv_list_basic() {
891 let mut doc = Document::new((1, 0));
892 let mut list = MatrixList::new(
893 "Person",
894 vec![
895 "id".to_string(),
896 "name".to_string(),
897 "age".to_string(),
898 "active".to_string(),
899 ],
900 );
901
902 list.add_row(Node::new(
903 "Person",
904 "1",
905 vec![
906 Value::String("1".into()),
907 Value::String("Alice".into()),
908 Value::Int(30),
909 Value::Bool(true),
910 ],
911 ));
912
913 list.add_row(Node::new(
914 "Person",
915 "2",
916 vec![
917 Value::String("2".into()),
918 Value::String("Bob".into()),
919 Value::Int(25),
920 Value::Bool(false),
921 ],
922 ));
923
924 doc.root.insert("people".to_string(), Item::List(list));
925
926 let csv = to_csv_list(&doc, "people").unwrap();
927 let expected = "id,name,age,active\n1,Alice,30,true\n2,Bob,25,false\n";
928 assert_eq!(csv, expected);
929 }
930
931 #[test]
932 fn test_to_csv_list_selective_export() {
933 let mut doc = Document::new((1, 0));
934
935 let mut people_list = MatrixList::new(
937 "Person",
938 vec!["id".to_string(), "name".to_string(), "age".to_string()],
939 );
940 people_list.add_row(Node::new(
941 "Person",
942 "1",
943 vec![
944 Value::String("1".into()),
945 Value::String("Alice".into()),
946 Value::Int(30),
947 ],
948 ));
949 doc.root
950 .insert("people".to_string(), Item::List(people_list));
951
952 let mut items_list = MatrixList::new(
954 "Item",
955 vec!["id".to_string(), "name".to_string(), "price".to_string()],
956 );
957 items_list.add_row(Node::new(
958 "Item",
959 "101",
960 vec![
961 Value::String("101".into()),
962 Value::String("Widget".into()),
963 Value::Float(9.99),
964 ],
965 ));
966 doc.root.insert("items".to_string(), Item::List(items_list));
967
968 let csv_people = to_csv_list(&doc, "people").unwrap();
970 assert!(csv_people.contains("Alice"));
971 assert!(!csv_people.contains("Widget"));
972
973 let csv_items = to_csv_list(&doc, "items").unwrap();
975 assert!(csv_items.contains("Widget"));
976 assert!(!csv_items.contains("Alice"));
977 }
978
979 #[test]
980 fn test_to_csv_list_not_found() {
981 let doc = Document::new((1, 0));
982 let result = to_csv_list(&doc, "nonexistent");
983
984 assert!(result.is_err());
985 let err = result.unwrap_err();
986 assert!(matches!(
987 err,
988 CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }
989 ));
990 assert!(err.to_string().contains("not found"));
991 }
992
993 #[test]
994 fn test_to_csv_list_not_a_list() {
995 let mut doc = Document::new((1, 0));
996 doc.root
997 .insert("scalar".to_string(), Item::Scalar(Value::Int(42)));
998
999 let result = to_csv_list(&doc, "scalar");
1000 assert!(result.is_err());
1001 let err = result.unwrap_err();
1002 assert!(matches!(
1003 err,
1004 CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }
1005 ));
1006 assert!(err.to_string().contains("not a matrix list"));
1007 }
1008
1009 #[test]
1010 fn test_to_csv_list_without_headers() {
1011 let mut doc = Document::new((1, 0));
1012 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1013
1014 list.add_row(Node::new(
1015 "Person",
1016 "1",
1017 vec![Value::String("1".into()), Value::String("Alice".into())],
1018 ));
1019
1020 doc.root.insert("people".to_string(), Item::List(list));
1021
1022 let config = ToCsvConfig {
1023 include_headers: false,
1024 ..Default::default()
1025 };
1026 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
1027
1028 let expected = "1,Alice\n";
1029 assert_eq!(csv, expected);
1030 }
1031
1032 #[test]
1033 fn test_to_csv_list_custom_delimiter() {
1034 let mut doc = Document::new((1, 0));
1035 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1036
1037 list.add_row(Node::new(
1038 "Person",
1039 "1",
1040 vec![Value::String("1".into()), Value::String("Alice".into())],
1041 ));
1042
1043 doc.root.insert("people".to_string(), Item::List(list));
1044
1045 let config = ToCsvConfig {
1046 delimiter: b';',
1047 ..Default::default()
1048 };
1049 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
1050
1051 let expected = "id;name\n1;Alice\n";
1052 assert_eq!(csv, expected);
1053 }
1054
1055 #[test]
1056 fn test_to_csv_list_tab_delimiter() {
1057 let mut doc = Document::new((1, 0));
1058 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1059
1060 list.add_row(Node::new(
1061 "Person",
1062 "1",
1063 vec![Value::String("1".into()), Value::String("Alice".into())],
1064 ));
1065
1066 doc.root.insert("people".to_string(), Item::List(list));
1067
1068 let config = ToCsvConfig {
1069 delimiter: b'\t',
1070 ..Default::default()
1071 };
1072 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
1073
1074 assert!(csv.contains("id\tname"));
1075 assert!(csv.contains("1\tAlice"));
1076 }
1077
1078 #[test]
1079 fn test_to_csv_list_empty() {
1080 let mut doc = Document::new((1, 0));
1081 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1082 doc.root.insert("people".to_string(), Item::List(list));
1083
1084 let csv = to_csv_list(&doc, "people").unwrap();
1085 let expected = "id,name\n";
1086 assert_eq!(csv, expected);
1087 }
1088
1089 #[test]
1090 fn test_to_csv_list_empty_no_headers() {
1091 let mut doc = Document::new((1, 0));
1092 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1093 doc.root.insert("people".to_string(), Item::List(list));
1094
1095 let config = ToCsvConfig {
1096 include_headers: false,
1097 ..Default::default()
1098 };
1099 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
1100 assert!(csv.is_empty());
1101 }
1102
1103 #[test]
1104 fn test_to_csv_list_writer() {
1105 let mut doc = Document::new((1, 0));
1106 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1107
1108 list.add_row(Node::new(
1109 "Person",
1110 "1",
1111 vec![Value::String("1".into()), Value::String("Alice".into())],
1112 ));
1113
1114 doc.root.insert("people".to_string(), Item::List(list));
1115
1116 let mut buffer = Vec::new();
1117 to_csv_list_writer(&doc, "people", &mut buffer).unwrap();
1118
1119 let csv = String::from_utf8(buffer).unwrap();
1120 assert!(csv.contains("Alice"));
1121 }
1122
1123 #[test]
1124 fn test_to_csv_list_writer_with_config() {
1125 let mut doc = Document::new((1, 0));
1126 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1127
1128 list.add_row(Node::new(
1129 "Person",
1130 "1",
1131 vec![Value::String("1".into()), Value::String("Alice".into())],
1132 ));
1133
1134 doc.root.insert("people".to_string(), Item::List(list));
1135
1136 let config = ToCsvConfig {
1137 include_headers: false,
1138 ..Default::default()
1139 };
1140 let mut buffer = Vec::new();
1141 to_csv_list_writer_with_config(&doc, "people", &mut buffer, config).unwrap();
1142
1143 let csv = String::from_utf8(buffer).unwrap();
1144 assert_eq!(csv, "1,Alice\n");
1145 }
1146
1147 #[test]
1148 fn test_to_csv_list_with_all_value_types() {
1149 let mut doc = Document::new((1, 0));
1150 let mut list = MatrixList::new(
1151 "Data",
1152 vec![
1153 "id".to_string(),
1154 "bool_val".to_string(),
1155 "int_val".to_string(),
1156 "float_val".to_string(),
1157 "string_val".to_string(),
1158 "null_val".to_string(),
1159 "ref_val".to_string(),
1160 ],
1161 );
1162
1163 list.add_row(Node::new(
1164 "Data",
1165 "1",
1166 vec![
1167 Value::String("1".into()),
1168 Value::Bool(true),
1169 Value::Int(42),
1170 Value::Float(3.5),
1171 Value::String("hello".into()),
1172 Value::Null,
1173 Value::Reference(Reference::local("user1")),
1174 ],
1175 ));
1176
1177 doc.root.insert("data".to_string(), Item::List(list));
1178
1179 let csv = to_csv_list(&doc, "data").unwrap();
1180 assert!(csv.contains("true"));
1181 assert!(csv.contains("42"));
1182 assert!(csv.contains("3.5"));
1183 assert!(csv.contains("hello"));
1184 assert!(csv.contains("@user1"));
1185 }
1186
1187 #[test]
1188 fn test_to_csv_list_with_nested_children_skipped() {
1189 let mut doc = Document::new((1, 0));
1190 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1191
1192 let mut person = Node::new(
1193 "Person",
1194 "1",
1195 vec![Value::String("1".into()), Value::String("Alice".into())],
1196 );
1197
1198 let child = Node::new(
1200 "Address",
1201 "addr1",
1202 vec![
1203 Value::String("addr1".into()),
1204 Value::String("123 Main St".into()),
1205 ],
1206 );
1207 person.add_child("Address", child);
1208
1209 list.add_row(person);
1210 doc.root.insert("people".to_string(), Item::List(list));
1211
1212 let csv = to_csv_list(&doc, "people").unwrap();
1214 assert!(csv.contains("Alice"));
1215 assert!(!csv.contains("Address"));
1216 assert!(!csv.contains("123 Main St"));
1217 }
1218
1219 #[test]
1220 fn test_to_csv_list_complex_quoting() {
1221 let mut doc = Document::new((1, 0));
1222 let mut list = MatrixList::new("Item", vec!["id".to_string(), "description".to_string()]);
1223
1224 list.add_row(Node::new(
1225 "Item",
1226 "1",
1227 vec![
1228 Value::String("1".into()),
1229 Value::String("Contains, comma and \"quotes\"".into()),
1230 ],
1231 ));
1232
1233 doc.root.insert("items".to_string(), Item::List(list));
1234
1235 let csv = to_csv_list(&doc, "items").unwrap();
1236 assert!(csv.contains("comma"));
1238 }
1239
1240 #[test]
1241 fn test_to_csv_list_multiple_lists_independent() {
1242 let mut doc = Document::new((1, 0));
1243
1244 let mut list1 = MatrixList::new("Type1", vec!["id".to_string(), "val".to_string()]);
1246 list1.add_row(Node::new(
1247 "Type1",
1248 "1",
1249 vec![Value::String("1".into()), Value::String("alpha".into())],
1250 ));
1251 list1.add_row(Node::new(
1252 "Type1",
1253 "2",
1254 vec![Value::String("2".into()), Value::String("bravo".into())],
1255 ));
1256 doc.root.insert("list1".to_string(), Item::List(list1));
1257
1258 let mut list2 = MatrixList::new("Type2", vec!["id".to_string(), "val".to_string()]);
1260 list2.add_row(Node::new(
1261 "Type2",
1262 "1",
1263 vec![Value::String("1".into()), Value::String("x_ray".into())],
1264 ));
1265 list2.add_row(Node::new(
1266 "Type2",
1267 "2",
1268 vec![Value::String("2".into()), Value::String("yankee".into())],
1269 ));
1270 list2.add_row(Node::new(
1271 "Type2",
1272 "3",
1273 vec![Value::String("3".into()), Value::String("zulu".into())],
1274 ));
1275 doc.root.insert("list2".to_string(), Item::List(list2));
1276
1277 let csv1 = to_csv_list(&doc, "list1").unwrap();
1279 let csv2 = to_csv_list(&doc, "list2").unwrap();
1280
1281 let lines1: Vec<&str> = csv1.lines().collect();
1283 assert_eq!(lines1.len(), 3); let lines2: Vec<&str> = csv2.lines().collect();
1287 assert_eq!(lines2.len(), 4); assert!(csv1.contains("alpha") && csv1.contains("bravo"));
1291 assert!(csv2.contains("x_ray") && csv2.contains("yankee") && csv2.contains("zulu"));
1292 assert!(!csv1.contains("x_ray"));
1293 assert!(!csv2.contains("alpha"));
1294 }
1295
1296 #[test]
1297 fn test_to_csv_list_special_floats() {
1298 let mut doc = Document::new((1, 0));
1299 let mut list = MatrixList::new("Data", vec!["id".to_string(), "value".to_string()]);
1300
1301 list.add_row(Node::new(
1302 "Data",
1303 "1",
1304 vec![Value::String("1".into()), Value::Float(f64::NAN)],
1305 ));
1306
1307 list.add_row(Node::new(
1308 "Data",
1309 "2",
1310 vec![Value::String("2".into()), Value::Float(f64::INFINITY)],
1311 ));
1312
1313 list.add_row(Node::new(
1314 "Data",
1315 "3",
1316 vec![Value::String("3".into()), Value::Float(f64::NEG_INFINITY)],
1317 ));
1318
1319 doc.root.insert("data".to_string(), Item::List(list));
1320
1321 let csv = to_csv_list(&doc, "data").unwrap();
1322 assert!(csv.contains("NaN"));
1323 assert!(csv.contains("Infinity"));
1324 assert!(csv.contains("-Infinity"));
1325 }
1326}