Skip to main content

hedl_csv/
to_csv.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6//
7// Licensed under the Apache License, Version 2.0 (the "License");
8// you may not use this file except in compliance with the License.
9// You may obtain a copy of the License in the LICENSE file at the
10// root of this repository or at: http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Convert HEDL documents to CSV format.
19
20use crate::error::{CsvError, Result};
21use hedl_core::{Document, MatrixList, Tensor, Value};
22use std::io::Write;
23
24/// Configuration for CSV output.
25#[derive(Debug, Clone)]
26pub struct ToCsvConfig {
27    /// Field delimiter (default: ',')
28    pub delimiter: u8,
29    /// Include header row (default: true)
30    pub include_headers: bool,
31    /// Quote style for fields (default: necessary)
32    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
45/// Convert a HEDL document to CSV string.
46///
47/// # Example
48/// ```no_run
49/// use hedl_core::Document;
50/// use hedl_csv::to_csv;
51///
52/// let doc = Document::new((1, 0));
53/// let csv_string = to_csv(&doc).unwrap();
54/// ```
55pub fn to_csv(doc: &Document) -> Result<String> {
56    to_csv_with_config(doc, ToCsvConfig::default())
57}
58
59/// Convert a specific matrix list from a HEDL document to CSV string.
60///
61/// Exports only the specified named list to CSV format, with configurable
62/// header row and delimiter options. Nested children are skipped with a warning
63/// (logged as part of error handling if strict mode desired).
64///
65/// # Arguments
66///
67/// * `doc` - The HEDL document
68/// * `list_name` - The name of the matrix list to export (e.g., "people", "items")
69///
70/// # Returns
71///
72/// A CSV-formatted string containing the specified list data
73///
74/// # Errors
75///
76/// Returns `HedlError` if:
77/// - The named list does not exist in the document
78/// - The list is not a `MatrixList` (i.e., it's a scalar or object)
79/// - CSV serialization fails
80///
81/// # Example
82/// ```no_run
83/// use hedl_core::Document;
84/// use hedl_csv::to_csv_list;
85///
86/// let doc = Document::new((1, 0));
87/// let csv_string = to_csv_list(&doc, "people").unwrap();
88/// println!("{}", csv_string);
89/// ```
90pub fn to_csv_list(doc: &Document, list_name: &str) -> Result<String> {
91    to_csv_list_with_config(doc, list_name, ToCsvConfig::default())
92}
93
94/// Convert a specific matrix list from a HEDL document to CSV string with custom configuration.
95///
96/// # Arguments
97///
98/// * `doc` - The HEDL document
99/// * `list_name` - The name of the matrix list to export
100/// * `config` - Custom CSV configuration (delimiter, headers, quote style)
101///
102/// # Example
103/// ```no_run
104/// use hedl_core::Document;
105/// use hedl_csv::{to_csv_list_with_config, ToCsvConfig};
106///
107/// let doc = Document::new((1, 0));
108/// let config = ToCsvConfig {
109///     delimiter: b';',
110///     include_headers: true,
111///     ..Default::default()
112/// };
113/// let csv_string = to_csv_list_with_config(&doc, "people", config).unwrap();
114/// ```
115pub 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
129/// Write a specific matrix list to CSV format using a writer.
130///
131/// # Arguments
132///
133/// * `doc` - The HEDL document
134/// * `list_name` - The name of the matrix list to export
135/// * `writer` - The output writer (file, buffer, etc.)
136///
137/// # Example
138/// ```no_run
139/// use hedl_core::Document;
140/// use hedl_csv::to_csv_list_writer;
141/// use std::fs::File;
142///
143/// let doc = Document::new((1, 0));
144/// let file = File::create("output.csv").unwrap();
145/// to_csv_list_writer(&doc, "people", file).unwrap();
146/// ```
147pub 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
151/// Write a specific matrix list to CSV format with custom configuration.
152///
153/// # Arguments
154///
155/// * `doc` - The HEDL document
156/// * `list_name` - The name of the matrix list to export
157/// * `writer` - The output writer (file, buffer, etc.)
158/// * `config` - Custom CSV configuration
159pub 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    // Find the specified list
166    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    // Write header row if requested
174    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    // Write each row, skipping nested children
183    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        // Note: Nested children in node.children are intentionally skipped.
194        // If a caller needs to export nested data, they should export those lists separately.
195    }
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
206/// Convert a HEDL document to CSV string with custom configuration.
207/// P1 OPTIMIZATION: Pre-allocate buffer capacity (1.1-1.2x speedup)
208pub fn to_csv_with_config(doc: &Document, config: ToCsvConfig) -> Result<String> {
209    // Estimate output size based on matrix list size
210    // Approximate: rows * columns * 20 bytes/cell (conservative estimate)
211    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
220/// Estimate CSV output size for pre-allocation
221fn estimate_csv_size(doc: &Document) -> usize {
222    let mut total = 0;
223
224    // Scan for matrix lists and estimate size
225    for item in doc.root.values() {
226        if let Some(list) = item.as_list() {
227            // Header row: column names + commas + newline
228            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            // Data rows: conservative estimate of 20 bytes per cell
237            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    // Return at least 1KB, max estimated size
246    total.max(1024)
247}
248
249/// Estimate CSV output size for a specific list
250fn 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            // Header row: column names + commas + newline
254            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            // Data rows: conservative estimate of 20 bytes per cell
263            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    // Fallback to minimal size
272    1024
273}
274
275/// Write a HEDL document to CSV format using a writer.
276///
277/// # Example
278/// ```no_run
279/// use hedl_core::Document;
280/// use hedl_csv::to_csv_writer;
281/// use std::fs::File;
282///
283/// let doc = Document::new((1, 0));
284/// let file = File::create("output.csv").unwrap();
285/// to_csv_writer(&doc, file).unwrap();
286/// ```
287pub fn to_csv_writer<W: Write>(doc: &Document, writer: W) -> Result<()> {
288    to_csv_writer_with_config(doc, writer, ToCsvConfig::default())
289}
290
291/// Write a HEDL document to CSV format with custom configuration.
292pub 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    // Find the first matrix list in the document
303    let matrix_list = find_first_matrix_list(doc)?;
304
305    // Write header row if requested
306    // Per SPEC.md: MatrixList.schema includes all column names with ID first
307    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    // Write each row
313    // Per SPEC.md: Node.fields contains ALL values including ID (first column)
314    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
331/// Find the first matrix list in the document.
332fn 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
342/// Find a matrix list by name in the document.
343fn 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
372/// Convert a HEDL value to CSV string representation.
373fn 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            // Handle special float values
380            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
400/// Convert a tensor to JSON-like array string representation.
401/// Examples: `[1,2,3]` or `[[1,2],[3,4]]`
402fn 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 as integer if it's a whole number
407                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
419/// Convert a list to HEDL list syntax string representation.
420/// Examples: `(admin, editor)` or `(true, false, true)`
421fn 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        // Per SPEC.md: MatrixList.schema includes all column names with ID first
447        // Node.fields contains ALL values including ID (first column)
448        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    // ==================== ToCsvConfig tests ====================
485
486    #[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    // ==================== to_csv basic tests ====================
528
529    #[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    // ==================== value_to_csv_string tests ====================
602
603    #[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        // The CSV library will quote this, but value_to_csv_string just returns the raw value
669        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    // ==================== Special float values ====================
720
721    #[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    // ==================== Tensor tests ====================
743
744    #[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    // ==================== Error cases ====================
791
792    #[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    // ==================== to_csv_writer tests ====================
820
821    #[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    // ==================== Quoting tests ====================
848
849    #[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        // The CSV library should quote fields with commas
865        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        // The CSV library should quote fields with newlines
884        assert!(csv.contains("\"line1\nline2\""));
885    }
886
887    // ==================== to_csv_list tests ====================
888
889    #[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        // Add first list
936        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        // Add second list
953        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        // Export only people
969        let csv_people = to_csv_list(&doc, "people").unwrap();
970        assert!(csv_people.contains("Alice"));
971        assert!(!csv_people.contains("Widget"));
972
973        // Export only items
974        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        // Add nested children (should be skipped in CSV export)
1199        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        // CSV should only contain the parent row, not the nested children
1213        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        // CSV library should handle quoting
1237        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        // First list with 2 rows
1245        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        // Second list with 3 rows
1259        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        // Export each list independently
1278        let csv1 = to_csv_list(&doc, "list1").unwrap();
1279        let csv2 = to_csv_list(&doc, "list2").unwrap();
1280
1281        // List1 should have 2 data rows
1282        let lines1: Vec<&str> = csv1.lines().collect();
1283        assert_eq!(lines1.len(), 3); // header + 2 rows
1284
1285        // List2 should have 3 data rows
1286        let lines2: Vec<&str> = csv2.lines().collect();
1287        assert_eq!(lines2.len(), 4); // header + 3 rows
1288
1289        // Each should contain only its own data
1290        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}