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(|_| {
125 CsvError::InvalidUtf8 {
126 context: "CSV output".to_string(),
127 }
128 })
129}
130
131pub fn to_csv_list_writer<W: Write>(
150 doc: &Document,
151 list_name: &str,
152 writer: W,
153) -> Result<()> {
154 to_csv_list_writer_with_config(doc, list_name, writer, ToCsvConfig::default())
155}
156
157pub fn to_csv_list_writer_with_config<W: Write>(
166 doc: &Document,
167 list_name: &str,
168 writer: W,
169 config: ToCsvConfig,
170) -> Result<()> {
171 let matrix_list = find_matrix_list_by_name(doc, list_name)?;
173
174 let mut wtr = csv::WriterBuilder::new()
175 .delimiter(config.delimiter)
176 .quote_style(config.quote_style)
177 .from_writer(writer);
178
179 if config.include_headers {
181 wtr.write_record(&matrix_list.schema).map_err(|e| {
182 CsvError::Other(format!(
183 "Failed to write CSV header for list '{}': {}",
184 list_name, e
185 ))
186 })?;
187 }
188
189 for node in &matrix_list.rows {
191 let record: Vec<String> = node.fields.iter().map(value_to_csv_string).collect();
192
193 wtr.write_record(&record).map_err(|e| {
194 CsvError::Other(format!(
195 "Failed to write CSV record for id '{}' in list '{}': {}",
196 node.id, list_name, e
197 ))
198 })?;
199
200 }
203
204 wtr.flush().map_err(|e| {
205 CsvError::Other(format!("Failed to flush CSV writer for list '{}': {}", list_name, e))
206 })?;
207
208 Ok(())
209}
210
211pub fn to_csv_with_config(doc: &Document, config: ToCsvConfig) -> Result<String> {
214 let estimated_size = estimate_csv_size(doc);
217 let mut buffer = Vec::with_capacity(estimated_size);
218
219 to_csv_writer_with_config(doc, &mut buffer, config)?;
220 String::from_utf8(buffer).map_err(|_| {
221 CsvError::InvalidUtf8 {
222 context: "CSV output".to_string(),
223 }
224 })
225}
226
227fn estimate_csv_size(doc: &Document) -> usize {
229 let mut total = 0;
230
231 for item in doc.root.values() {
233 if let Some(list) = item.as_list() {
234 let header_size = list.schema.iter().map(|s| s.len()).sum::<usize>()
236 + list.schema.len()
237 + 1;
238
239 let row_count = list.rows.len();
241 let col_count = list.schema.len();
242 let data_size = row_count * col_count * 20;
243
244 total += header_size + data_size;
245 }
246 }
247
248 total.max(1024)
250}
251
252fn estimate_list_csv_size(doc: &Document, list_name: &str) -> usize {
254 if let Some(item) = doc.root.get(list_name) {
255 if let Some(list) = item.as_list() {
256 let header_size = list.schema.iter().map(|s| s.len()).sum::<usize>()
258 + list.schema.len()
259 + 1;
260
261 let row_count = list.rows.len();
263 let col_count = list.schema.len();
264 let data_size = row_count * col_count * 20;
265
266 return (header_size + data_size).max(1024);
267 }
268 }
269
270 1024
272}
273
274pub fn to_csv_writer<W: Write>(doc: &Document, writer: W) -> Result<()> {
287 to_csv_writer_with_config(doc, writer, ToCsvConfig::default())
288}
289
290pub fn to_csv_writer_with_config<W: Write>(
292 doc: &Document,
293 writer: W,
294 config: ToCsvConfig,
295) -> Result<()> {
296 let mut wtr = csv::WriterBuilder::new()
297 .delimiter(config.delimiter)
298 .quote_style(config.quote_style)
299 .from_writer(writer);
300
301 let matrix_list = find_first_matrix_list(doc)?;
303
304 if config.include_headers {
307 wtr.write_record(&matrix_list.schema).map_err(|e| {
308 CsvError::Other(format!("Failed to write CSV header: {}", e))
309 })?;
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!("Failed to write CSV record for id '{}': {}", node.id, e))
319 })?;
320 }
321
322 wtr.flush().map_err(|e| {
323 CsvError::Other(format!("Failed to flush CSV writer: {}", e))
324 })?;
325
326 Ok(())
327}
328
329fn find_first_matrix_list(doc: &Document) -> Result<&MatrixList> {
331 for item in doc.root.values() {
332 if let Some(list) = item.as_list() {
333 return Ok(list);
334 }
335 }
336
337 Err(CsvError::NoLists)
338}
339
340fn find_matrix_list_by_name<'a>(doc: &'a Document, list_name: &str) -> Result<&'a MatrixList> {
342 match doc.root.get(list_name) {
343 Some(item) => match item.as_list() {
344 Some(list) => Ok(list),
345 None => Err(CsvError::NotAList {
346 name: list_name.to_string(),
347 actual_type: match item {
348 hedl_core::Item::Scalar(_) => "scalar",
349 hedl_core::Item::Object(_) => "object",
350 hedl_core::Item::List(_) => "list",
351 }.to_string(),
352 }),
353 },
354 None => Err(CsvError::ListNotFound {
355 name: list_name.to_string(),
356 available: if doc.root.is_empty() {
357 "none".to_string()
358 } else {
359 doc.root
360 .keys()
361 .map(|k| format!("'{}'", k))
362 .collect::<Vec<_>>()
363 .join(", ")
364 },
365 }),
366 }
367}
368
369fn value_to_csv_string(value: &Value) -> String {
371 match value {
372 Value::Null => String::new(),
373 Value::Bool(b) => b.to_string(),
374 Value::Int(n) => n.to_string(),
375 Value::Float(f) => {
376 if f.is_nan() {
378 "NaN".to_string()
379 } else if f.is_infinite() {
380 if f.is_sign_positive() {
381 "Infinity".to_string()
382 } else {
383 "-Infinity".to_string()
384 }
385 } else {
386 f.to_string()
387 }
388 }
389 Value::String(s) => s.clone(),
390 Value::Reference(r) => r.to_ref_string(),
391 Value::Tensor(t) => tensor_to_json_string(t),
392 Value::Expression(e) => format!("$({})", e),
393 }
394}
395
396fn tensor_to_json_string(tensor: &Tensor) -> String {
399 match tensor {
400 Tensor::Scalar(n) => {
401 if n.fract() == 0.0 && n.abs() < i64::MAX as f64 {
402 format!("{}", *n as i64)
404 } else {
405 format!("{}", n)
406 }
407 }
408 Tensor::Array(items) => {
409 let inner: Vec<String> = items.iter().map(tensor_to_json_string).collect();
410 format!("[{}]", inner.join(","))
411 }
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use hedl_core::{Document, Item, MatrixList, Node, Reference, Value};
419 use hedl_core::lex::{Expression, Span};
420
421 fn create_test_document() -> Document {
422 let mut doc = Document::new((1, 0));
423
424 let mut list = MatrixList::new(
427 "Person",
428 vec![
429 "id".to_string(),
430 "name".to_string(),
431 "age".to_string(),
432 "active".to_string(),
433 ],
434 );
435
436 list.add_row(Node::new(
437 "Person",
438 "1",
439 vec![
440 Value::String("1".to_string()),
441 Value::String("Alice".to_string()),
442 Value::Int(30),
443 Value::Bool(true),
444 ],
445 ));
446
447 list.add_row(Node::new(
448 "Person",
449 "2",
450 vec![
451 Value::String("2".to_string()),
452 Value::String("Bob".to_string()),
453 Value::Int(25),
454 Value::Bool(false),
455 ],
456 ));
457
458 doc.root.insert("people".to_string(), Item::List(list));
459 doc
460 }
461
462 #[test]
465 fn test_to_csv_config_default() {
466 let config = ToCsvConfig::default();
467 assert_eq!(config.delimiter, b',');
468 assert!(config.include_headers);
469 assert!(matches!(config.quote_style, csv::QuoteStyle::Necessary));
470 }
471
472 #[test]
473 fn test_to_csv_config_debug() {
474 let config = ToCsvConfig::default();
475 let debug = format!("{:?}", config);
476 assert!(debug.contains("ToCsvConfig"));
477 assert!(debug.contains("delimiter"));
478 assert!(debug.contains("include_headers"));
479 assert!(debug.contains("quote_style"));
480 }
481
482 #[test]
483 fn test_to_csv_config_clone() {
484 let config = ToCsvConfig {
485 delimiter: b'\t',
486 include_headers: false,
487 quote_style: csv::QuoteStyle::Always,
488 };
489 let cloned = config.clone();
490 assert_eq!(cloned.delimiter, b'\t');
491 assert!(!cloned.include_headers);
492 }
493
494 #[test]
495 fn test_to_csv_config_all_options() {
496 let config = ToCsvConfig {
497 delimiter: b';',
498 include_headers: true,
499 quote_style: csv::QuoteStyle::Always,
500 };
501 assert_eq!(config.delimiter, b';');
502 assert!(config.include_headers);
503 }
504
505 #[test]
508 fn test_to_csv_basic() {
509 let doc = create_test_document();
510 let csv = to_csv(&doc).unwrap();
511
512 let expected = "id,name,age,active\n1,Alice,30,true\n2,Bob,25,false\n";
513 assert_eq!(csv, expected);
514 }
515
516 #[test]
517 fn test_to_csv_without_headers() {
518 let doc = create_test_document();
519 let config = ToCsvConfig {
520 include_headers: false,
521 ..Default::default()
522 };
523 let csv = to_csv_with_config(&doc, config).unwrap();
524
525 let expected = "1,Alice,30,true\n2,Bob,25,false\n";
526 assert_eq!(csv, expected);
527 }
528
529 #[test]
530 fn test_to_csv_custom_delimiter() {
531 let doc = create_test_document();
532 let config = ToCsvConfig {
533 delimiter: b'\t',
534 ..Default::default()
535 };
536 let csv = to_csv_with_config(&doc, config).unwrap();
537
538 let expected = "id\tname\tage\tactive\n1\tAlice\t30\ttrue\n2\tBob\t25\tfalse\n";
539 assert_eq!(csv, expected);
540 }
541
542 #[test]
543 fn test_to_csv_semicolon_delimiter() {
544 let doc = create_test_document();
545 let config = ToCsvConfig {
546 delimiter: b';',
547 ..Default::default()
548 };
549 let csv = to_csv_with_config(&doc, config).unwrap();
550
551 assert!(csv.contains(";"));
552 assert!(csv.contains("Alice"));
553 }
554
555 #[test]
556 fn test_to_csv_empty_list() {
557 let mut doc = Document::new((1, 0));
558 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
559 doc.root.insert("people".to_string(), Item::List(list));
560
561 let csv = to_csv(&doc).unwrap();
562 assert_eq!(csv, "id,name\n");
563 }
564
565 #[test]
566 fn test_to_csv_empty_list_no_headers() {
567 let mut doc = Document::new((1, 0));
568 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
569 doc.root.insert("people".to_string(), Item::List(list));
570
571 let config = ToCsvConfig {
572 include_headers: false,
573 ..Default::default()
574 };
575 let csv = to_csv_with_config(&doc, config).unwrap();
576 assert!(csv.is_empty());
577 }
578
579 #[test]
582 fn test_value_to_csv_string_null() {
583 assert_eq!(value_to_csv_string(&Value::Null), "");
584 }
585
586 #[test]
587 fn test_value_to_csv_string_bool_true() {
588 assert_eq!(value_to_csv_string(&Value::Bool(true)), "true");
589 }
590
591 #[test]
592 fn test_value_to_csv_string_bool_false() {
593 assert_eq!(value_to_csv_string(&Value::Bool(false)), "false");
594 }
595
596 #[test]
597 fn test_value_to_csv_string_int_positive() {
598 assert_eq!(value_to_csv_string(&Value::Int(42)), "42");
599 }
600
601 #[test]
602 fn test_value_to_csv_string_int_negative() {
603 assert_eq!(value_to_csv_string(&Value::Int(-100)), "-100");
604 }
605
606 #[test]
607 fn test_value_to_csv_string_int_zero() {
608 assert_eq!(value_to_csv_string(&Value::Int(0)), "0");
609 }
610
611 #[test]
612 fn test_value_to_csv_string_int_large() {
613 assert_eq!(
614 value_to_csv_string(&Value::Int(i64::MAX)),
615 i64::MAX.to_string()
616 );
617 }
618
619 #[test]
620 fn test_value_to_csv_string_float_positive() {
621 assert_eq!(value_to_csv_string(&Value::Float(3.25)), "3.25");
622 }
623
624 #[test]
625 fn test_value_to_csv_string_float_negative() {
626 assert_eq!(value_to_csv_string(&Value::Float(-2.5)), "-2.5");
627 }
628
629 #[test]
630 fn test_value_to_csv_string_float_zero() {
631 assert_eq!(value_to_csv_string(&Value::Float(0.0)), "0");
632 }
633
634 #[test]
635 fn test_value_to_csv_string_string() {
636 assert_eq!(
637 value_to_csv_string(&Value::String("hello".to_string())),
638 "hello"
639 );
640 }
641
642 #[test]
643 fn test_value_to_csv_string_string_empty() {
644 assert_eq!(value_to_csv_string(&Value::String("".to_string())), "");
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".to_string())),
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(Expression::Identifier {
675 name: "foo".to_string(),
676 span: Span::default(),
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(Expression::Call {
684 name: "add".to_string(),
685 args: vec![
686 Expression::Identifier {
687 name: "x".to_string(),
688 span: Span::default(),
689 },
690 Expression::Literal {
691 value: hedl_core::lex::ExprLiteral::Int(1),
692 span: Span::default(),
693 },
694 ],
695 span: Span::default(),
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!(value_to_csv_string(&Value::Tensor(tensor)), "[1,2]");
766 }
767
768 #[test]
771 fn test_no_matrix_list_error() {
772 let doc = Document::new((1, 0));
773 let result = to_csv(&doc);
774
775 assert!(result.is_err());
776 let err = result.unwrap_err();
777 assert!(matches!(err, CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }));
778 }
779
780 #[test]
781 fn test_no_matrix_list_with_scalar() {
782 let mut doc = Document::new((1, 0));
783 doc.root
784 .insert("value".to_string(), Item::Scalar(Value::Int(42)));
785
786 let result = to_csv(&doc);
787 assert!(result.is_err());
788 assert!(matches!(result.unwrap_err(), CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }));
789 }
790
791 #[test]
794 fn test_to_csv_writer_basic() {
795 let doc = create_test_document();
796 let mut buffer = Vec::new();
797 to_csv_writer(&doc, &mut buffer).unwrap();
798
799 let csv = String::from_utf8(buffer).unwrap();
800 assert!(csv.contains("Alice"));
801 assert!(csv.contains("Bob"));
802 }
803
804 #[test]
805 fn test_to_csv_writer_with_config() {
806 let doc = create_test_document();
807 let config = ToCsvConfig {
808 include_headers: false,
809 ..Default::default()
810 };
811 let mut buffer = Vec::new();
812 to_csv_writer_with_config(&doc, &mut buffer, config).unwrap();
813
814 let csv = String::from_utf8(buffer).unwrap();
815 assert!(!csv.contains("id,name"));
816 assert!(csv.contains("Alice"));
817 }
818
819 #[test]
822 fn test_quoting_with_comma() {
823 let mut doc = Document::new((1, 0));
824 let mut list = MatrixList::new("Item", vec!["id".to_string(), "text".to_string()]);
825 list.add_row(Node::new(
826 "Item",
827 "1",
828 vec![
829 Value::String("1".to_string()),
830 Value::String("hello, world".to_string()),
831 ],
832 ));
833 doc.root.insert("items".to_string(), Item::List(list));
834
835 let csv = to_csv(&doc).unwrap();
836 assert!(csv.contains("\"hello, world\""));
838 }
839
840 #[test]
841 fn test_quoting_with_newline() {
842 let mut doc = Document::new((1, 0));
843 let mut list = MatrixList::new("Item", vec!["id".to_string(), "text".to_string()]);
844 list.add_row(Node::new(
845 "Item",
846 "1",
847 vec![
848 Value::String("1".to_string()),
849 Value::String("line1\nline2".to_string()),
850 ],
851 ));
852 doc.root.insert("items".to_string(), Item::List(list));
853
854 let csv = to_csv(&doc).unwrap();
855 assert!(csv.contains("\"line1\nline2\""));
857 }
858
859 #[test]
862 fn test_to_csv_list_basic() {
863 let mut doc = Document::new((1, 0));
864 let mut list = MatrixList::new(
865 "Person",
866 vec![
867 "id".to_string(),
868 "name".to_string(),
869 "age".to_string(),
870 "active".to_string(),
871 ],
872 );
873
874 list.add_row(Node::new(
875 "Person",
876 "1",
877 vec![
878 Value::String("1".to_string()),
879 Value::String("Alice".to_string()),
880 Value::Int(30),
881 Value::Bool(true),
882 ],
883 ));
884
885 list.add_row(Node::new(
886 "Person",
887 "2",
888 vec![
889 Value::String("2".to_string()),
890 Value::String("Bob".to_string()),
891 Value::Int(25),
892 Value::Bool(false),
893 ],
894 ));
895
896 doc.root.insert("people".to_string(), Item::List(list));
897
898 let csv = to_csv_list(&doc, "people").unwrap();
899 let expected = "id,name,age,active\n1,Alice,30,true\n2,Bob,25,false\n";
900 assert_eq!(csv, expected);
901 }
902
903 #[test]
904 fn test_to_csv_list_selective_export() {
905 let mut doc = Document::new((1, 0));
906
907 let mut people_list = MatrixList::new(
909 "Person",
910 vec!["id".to_string(), "name".to_string(), "age".to_string()],
911 );
912 people_list.add_row(Node::new(
913 "Person",
914 "1",
915 vec![
916 Value::String("1".to_string()),
917 Value::String("Alice".to_string()),
918 Value::Int(30),
919 ],
920 ));
921 doc.root
922 .insert("people".to_string(), Item::List(people_list));
923
924 let mut items_list = MatrixList::new(
926 "Item",
927 vec!["id".to_string(), "name".to_string(), "price".to_string()],
928 );
929 items_list.add_row(Node::new(
930 "Item",
931 "101",
932 vec![
933 Value::String("101".to_string()),
934 Value::String("Widget".to_string()),
935 Value::Float(9.99),
936 ],
937 ));
938 doc.root.insert("items".to_string(), Item::List(items_list));
939
940 let csv_people = to_csv_list(&doc, "people").unwrap();
942 assert!(csv_people.contains("Alice"));
943 assert!(!csv_people.contains("Widget"));
944
945 let csv_items = to_csv_list(&doc, "items").unwrap();
947 assert!(csv_items.contains("Widget"));
948 assert!(!csv_items.contains("Alice"));
949 }
950
951 #[test]
952 fn test_to_csv_list_not_found() {
953 let doc = Document::new((1, 0));
954 let result = to_csv_list(&doc, "nonexistent");
955
956 assert!(result.is_err());
957 let err = result.unwrap_err();
958 assert!(matches!(err, CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }));
959 assert!(err.to_string().contains("not found"));
960 }
961
962 #[test]
963 fn test_to_csv_list_not_a_list() {
964 let mut doc = Document::new((1, 0));
965 doc.root
966 .insert("scalar".to_string(), Item::Scalar(Value::Int(42)));
967
968 let result = to_csv_list(&doc, "scalar");
969 assert!(result.is_err());
970 let err = result.unwrap_err();
971 assert!(matches!(err, CsvError::NoLists | CsvError::NotAList { .. } | CsvError::ListNotFound { .. }));
972 assert!(err.to_string().contains("not a matrix list"));
973 }
974
975 #[test]
976 fn test_to_csv_list_without_headers() {
977 let mut doc = Document::new((1, 0));
978 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
979
980 list.add_row(Node::new(
981 "Person",
982 "1",
983 vec![Value::String("1".to_string()), Value::String("Alice".to_string())],
984 ));
985
986 doc.root.insert("people".to_string(), Item::List(list));
987
988 let config = ToCsvConfig {
989 include_headers: false,
990 ..Default::default()
991 };
992 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
993
994 let expected = "1,Alice\n";
995 assert_eq!(csv, expected);
996 }
997
998 #[test]
999 fn test_to_csv_list_custom_delimiter() {
1000 let mut doc = Document::new((1, 0));
1001 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1002
1003 list.add_row(Node::new(
1004 "Person",
1005 "1",
1006 vec![Value::String("1".to_string()), Value::String("Alice".to_string())],
1007 ));
1008
1009 doc.root.insert("people".to_string(), Item::List(list));
1010
1011 let config = ToCsvConfig {
1012 delimiter: b';',
1013 ..Default::default()
1014 };
1015 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
1016
1017 let expected = "id;name\n1;Alice\n";
1018 assert_eq!(csv, expected);
1019 }
1020
1021 #[test]
1022 fn test_to_csv_list_tab_delimiter() {
1023 let mut doc = Document::new((1, 0));
1024 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1025
1026 list.add_row(Node::new(
1027 "Person",
1028 "1",
1029 vec![Value::String("1".to_string()), Value::String("Alice".to_string())],
1030 ));
1031
1032 doc.root.insert("people".to_string(), Item::List(list));
1033
1034 let config = ToCsvConfig {
1035 delimiter: b'\t',
1036 ..Default::default()
1037 };
1038 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
1039
1040 assert!(csv.contains("id\tname"));
1041 assert!(csv.contains("1\tAlice"));
1042 }
1043
1044 #[test]
1045 fn test_to_csv_list_empty() {
1046 let mut doc = Document::new((1, 0));
1047 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1048 doc.root.insert("people".to_string(), Item::List(list));
1049
1050 let csv = to_csv_list(&doc, "people").unwrap();
1051 let expected = "id,name\n";
1052 assert_eq!(csv, expected);
1053 }
1054
1055 #[test]
1056 fn test_to_csv_list_empty_no_headers() {
1057 let mut doc = Document::new((1, 0));
1058 let list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1059 doc.root.insert("people".to_string(), Item::List(list));
1060
1061 let config = ToCsvConfig {
1062 include_headers: false,
1063 ..Default::default()
1064 };
1065 let csv = to_csv_list_with_config(&doc, "people", config).unwrap();
1066 assert!(csv.is_empty());
1067 }
1068
1069 #[test]
1070 fn test_to_csv_list_writer() {
1071 let mut doc = Document::new((1, 0));
1072 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1073
1074 list.add_row(Node::new(
1075 "Person",
1076 "1",
1077 vec![Value::String("1".to_string()), Value::String("Alice".to_string())],
1078 ));
1079
1080 doc.root.insert("people".to_string(), Item::List(list));
1081
1082 let mut buffer = Vec::new();
1083 to_csv_list_writer(&doc, "people", &mut buffer).unwrap();
1084
1085 let csv = String::from_utf8(buffer).unwrap();
1086 assert!(csv.contains("Alice"));
1087 }
1088
1089 #[test]
1090 fn test_to_csv_list_writer_with_config() {
1091 let mut doc = Document::new((1, 0));
1092 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1093
1094 list.add_row(Node::new(
1095 "Person",
1096 "1",
1097 vec![Value::String("1".to_string()), Value::String("Alice".to_string())],
1098 ));
1099
1100 doc.root.insert("people".to_string(), Item::List(list));
1101
1102 let config = ToCsvConfig {
1103 include_headers: false,
1104 ..Default::default()
1105 };
1106 let mut buffer = Vec::new();
1107 to_csv_list_writer_with_config(&doc, "people", &mut buffer, config).unwrap();
1108
1109 let csv = String::from_utf8(buffer).unwrap();
1110 assert_eq!(csv, "1,Alice\n");
1111 }
1112
1113 #[test]
1114 fn test_to_csv_list_with_all_value_types() {
1115 let mut doc = Document::new((1, 0));
1116 let mut list = MatrixList::new(
1117 "Data",
1118 vec![
1119 "id".to_string(),
1120 "bool_val".to_string(),
1121 "int_val".to_string(),
1122 "float_val".to_string(),
1123 "string_val".to_string(),
1124 "null_val".to_string(),
1125 "ref_val".to_string(),
1126 ],
1127 );
1128
1129 list.add_row(Node::new(
1130 "Data",
1131 "1",
1132 vec![
1133 Value::String("1".to_string()),
1134 Value::Bool(true),
1135 Value::Int(42),
1136 Value::Float(3.14),
1137 Value::String("hello".to_string()),
1138 Value::Null,
1139 Value::Reference(Reference::local("user1")),
1140 ],
1141 ));
1142
1143 doc.root.insert("data".to_string(), Item::List(list));
1144
1145 let csv = to_csv_list(&doc, "data").unwrap();
1146 assert!(csv.contains("true"));
1147 assert!(csv.contains("42"));
1148 assert!(csv.contains("3.14"));
1149 assert!(csv.contains("hello"));
1150 assert!(csv.contains("@user1"));
1151 }
1152
1153 #[test]
1154 fn test_to_csv_list_with_nested_children_skipped() {
1155 let mut doc = Document::new((1, 0));
1156 let mut list = MatrixList::new("Person", vec!["id".to_string(), "name".to_string()]);
1157
1158 let mut person = Node::new(
1159 "Person",
1160 "1",
1161 vec![
1162 Value::String("1".to_string()),
1163 Value::String("Alice".to_string()),
1164 ],
1165 );
1166
1167 let child = Node::new(
1169 "Address",
1170 "addr1",
1171 vec![Value::String("addr1".to_string()), Value::String("123 Main St".to_string())],
1172 );
1173 person.add_child("Address", child);
1174
1175 list.add_row(person);
1176 doc.root.insert("people".to_string(), Item::List(list));
1177
1178 let csv = to_csv_list(&doc, "people").unwrap();
1180 assert!(csv.contains("Alice"));
1181 assert!(!csv.contains("Address"));
1182 assert!(!csv.contains("123 Main St"));
1183 }
1184
1185 #[test]
1186 fn test_to_csv_list_complex_quoting() {
1187 let mut doc = Document::new((1, 0));
1188 let mut list = MatrixList::new("Item", vec!["id".to_string(), "description".to_string()]);
1189
1190 list.add_row(Node::new(
1191 "Item",
1192 "1",
1193 vec![
1194 Value::String("1".to_string()),
1195 Value::String("Contains, comma and \"quotes\"".to_string()),
1196 ],
1197 ));
1198
1199 doc.root.insert("items".to_string(), Item::List(list));
1200
1201 let csv = to_csv_list(&doc, "items").unwrap();
1202 assert!(csv.contains("comma"));
1204 }
1205
1206 #[test]
1207 fn test_to_csv_list_multiple_lists_independent() {
1208 let mut doc = Document::new((1, 0));
1209
1210 let mut list1 = MatrixList::new("Type1", vec!["id".to_string(), "val".to_string()]);
1212 list1.add_row(Node::new(
1213 "Type1",
1214 "1",
1215 vec![Value::String("1".to_string()), Value::String("alpha".to_string())],
1216 ));
1217 list1.add_row(Node::new(
1218 "Type1",
1219 "2",
1220 vec![Value::String("2".to_string()), Value::String("bravo".to_string())],
1221 ));
1222 doc.root.insert("list1".to_string(), Item::List(list1));
1223
1224 let mut list2 = MatrixList::new("Type2", vec!["id".to_string(), "val".to_string()]);
1226 list2.add_row(Node::new(
1227 "Type2",
1228 "1",
1229 vec![Value::String("1".to_string()), Value::String("x_ray".to_string())],
1230 ));
1231 list2.add_row(Node::new(
1232 "Type2",
1233 "2",
1234 vec![Value::String("2".to_string()), Value::String("yankee".to_string())],
1235 ));
1236 list2.add_row(Node::new(
1237 "Type2",
1238 "3",
1239 vec![Value::String("3".to_string()), Value::String("zulu".to_string())],
1240 ));
1241 doc.root.insert("list2".to_string(), Item::List(list2));
1242
1243 let csv1 = to_csv_list(&doc, "list1").unwrap();
1245 let csv2 = to_csv_list(&doc, "list2").unwrap();
1246
1247 let lines1: Vec<&str> = csv1.lines().collect();
1249 assert_eq!(lines1.len(), 3); let lines2: Vec<&str> = csv2.lines().collect();
1253 assert_eq!(lines2.len(), 4); assert!(csv1.contains("alpha") && csv1.contains("bravo"));
1257 assert!(csv2.contains("x_ray") && csv2.contains("yankee") && csv2.contains("zulu"));
1258 assert!(!csv1.contains("x_ray"));
1259 assert!(!csv2.contains("alpha"));
1260 }
1261
1262 #[test]
1263 fn test_to_csv_list_special_floats() {
1264 let mut doc = Document::new((1, 0));
1265 let mut list = MatrixList::new("Data", vec!["id".to_string(), "value".to_string()]);
1266
1267 list.add_row(Node::new(
1268 "Data",
1269 "1",
1270 vec![Value::String("1".to_string()), Value::Float(f64::NAN)],
1271 ));
1272
1273 list.add_row(Node::new(
1274 "Data",
1275 "2",
1276 vec![Value::String("2".to_string()), Value::Float(f64::INFINITY)],
1277 ));
1278
1279 list.add_row(Node::new(
1280 "Data",
1281 "3",
1282 vec![
1283 Value::String("3".to_string()),
1284 Value::Float(f64::NEG_INFINITY),
1285 ],
1286 ));
1287
1288 doc.root.insert("data".to_string(), Item::List(list));
1289
1290 let csv = to_csv_list(&doc, "data").unwrap();
1291 assert!(csv.contains("NaN"));
1292 assert!(csv.contains("Infinity"));
1293 assert!(csv.contains("-Infinity"));
1294 }
1295}