Skip to main content

tabnas_render/
csv.rs

1//! `TableRows/1` as CSV: the always-quoted profile.
2//!
3//! The standard profile quotes every field, doubles `"`, ends every record
4//! with CRLF and writes numbers as their lexemes. Always quoting is a
5//! decision, not a habit: it makes the output independent of the data (no
6//! field can change the record's shape), it makes the empty string and the
7//! null text distinguishable from a missing quote pair, and it lets a
8//! reader tell that a field was a field. Minimal quoting and other
9//! delimiters are dialects the caller selects explicitly, and they are
10//! valid here because a row is a finite vector: the renderer sees the whole
11//! field before it decides how to write it.
12//!
13//! The renderer validates the protocol as it goes, because a third-party
14//! transducer or a host adapter is as much a source of `TableRows/1` as the
15//! standard table transducer is.
16
17use tabnas_transduce::{Cell, Code, Fail, Flow, PublicColumn, TableEvent, TableSink};
18
19use crate::number::{check_number, write_value};
20use crate::text::TextOut;
21
22/// The record terminator.
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub enum Newline {
25    Lf,
26    /// RFC 4180's terminator, and the standard profile's.
27    #[default]
28    CrLf,
29}
30
31impl Newline {
32    pub fn as_str(self) -> &'static str {
33        match self {
34            Newline::Lf => "\n",
35            Newline::CrLf => "\r\n",
36        }
37    }
38}
39
40/// When a field is quoted.
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub enum Quoting {
43    /// Every field, the standard profile.
44    #[default]
45    Always,
46    /// Only a field holding the delimiter, `"`, CR or LF. An empty field is
47    /// then written as nothing, so the empty string and an empty null text
48    /// read back the same; that is the dialect's trade-off, not a defect.
49    Minimal,
50}
51
52/// What a [`Cell::Missing`] becomes.
53#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub enum MissingText {
55    /// Fail the run with `MISSING_VALUE`: a table that promised a column
56    /// and did not deliver it is not silently padded.
57    #[default]
58    Error,
59    /// Write this text instead.
60    Text(Box<str>),
61}
62
63/// The CSV dialect.
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct CsvOptions {
66    /// One character, and not `"`, CR, LF or NUL: those would make the
67    /// output unreadable by construction, and are refused when the renderer
68    /// is built.
69    pub delimiter: char,
70    pub newline: Newline,
71    /// Write the labels as the first record.
72    pub header: bool,
73    /// The text of a [`Cell::Null`]; empty by default.
74    pub null_text: Box<str>,
75    pub missing: MissingText,
76    pub quoting: Quoting,
77}
78
79impl Default for CsvOptions {
80    fn default() -> Self {
81        CsvOptions {
82            delimiter: ',',
83            newline: Newline::CrLf,
84            header: true,
85            null_text: "".into(),
86            missing: MissingText::Error,
87            quoting: Quoting::Always,
88        }
89    }
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93enum Phase {
94    BeforeSchema,
95    Rows,
96    Done,
97}
98
99/// Renders `TableRows/1` as CSV.
100///
101/// One schema first, rows exactly as wide as the schema, one end: anything
102/// else is `PROTOCOL_ORDER_ERROR`. A schema with no columns has no CSV form
103/// (a record cannot be empty) and is `TARGET_VALUE_UNREPRESENTABLE`. Every
104/// record, the header included, ends with the configured newline, the last
105/// one too. The output is flushed once, at `End`, so a table that fails
106/// half way is not flushed as if it were whole; a failure found after any
107/// text was written says so with `committed_output`.
108pub struct CsvRenderer<O: TextOut> {
109    out: O,
110    options: CsvOptions,
111    delimiter: String,
112    phase: Phase,
113    labels: Vec<Box<str>>,
114    rows: u64,
115    emitted: bool,
116    scratch: String,
117}
118
119impl<O: TextOut> CsvRenderer<O> {
120    /// A renderer over `out`, or `TARGET_VALUE_UNREPRESENTABLE` when the
121    /// delimiter is one no CSV reader could take.
122    pub fn new(out: O, options: CsvOptions) -> Result<Self, Fail> {
123        if matches!(options.delimiter, '"' | '\r' | '\n' | '\0') {
124            return Err(Fail::new(
125                Code::TargetValueUnrepresentable,
126                format!(
127                    "{:?} cannot be a CSV delimiter: it is the quote, a line break or NUL",
128                    options.delimiter
129                ),
130            ));
131        }
132        Ok(CsvRenderer {
133            delimiter: options.delimiter.to_string(),
134            out,
135            options,
136            phase: Phase::BeforeSchema,
137            labels: Vec::new(),
138            rows: 0,
139            emitted: false,
140            scratch: String::new(),
141        })
142    }
143
144    pub fn options(&self) -> &CsvOptions {
145        &self.options
146    }
147
148    /// Rows written so far.
149    pub fn rows(&self) -> u64 {
150        self.rows
151    }
152
153    /// Whether `End` has been rendered.
154    pub fn is_done(&self) -> bool {
155        self.phase == Phase::Done
156    }
157
158    pub fn into_inner(self) -> O {
159        self.out
160    }
161
162    /// Mark a failure as leaving partial output when text this renderer
163    /// wrote has reached the destination; text still buffered in the
164    /// output has not, and the output knows which.
165    fn fail(&self, f: Fail) -> Fail {
166        if self.emitted && self.out.has_committed() {
167            f.committed()
168        } else {
169            f
170        }
171    }
172
173    fn schema(&mut self, columns: &[PublicColumn]) -> Result<(), Fail> {
174        match self.phase {
175            Phase::BeforeSchema => {}
176            Phase::Rows => return Err(self.fail(Fail::protocol("a second schema"))),
177            Phase::Done => return Err(self.fail(Fail::protocol("a schema after the end"))),
178        }
179        if columns.is_empty() {
180            return Err(Fail::new(
181                Code::TargetValueUnrepresentable,
182                "a table with no columns has no CSV form",
183            ));
184        }
185        self.labels = columns.iter().map(|c| c.label.clone()).collect();
186        self.phase = Phase::Rows;
187        if self.options.header {
188            self.emitted = true;
189            for (i, label) in self.labels.iter().enumerate() {
190                if i > 0 {
191                    self.out.write_str(&self.delimiter)?;
192                }
193                write_field(
194                    &mut self.out,
195                    self.options.quoting,
196                    self.options.delimiter,
197                    label,
198                )?;
199            }
200            self.out.write_str(self.options.newline.as_str())?;
201        }
202        Ok(())
203    }
204
205    fn row(&mut self, cells: &[Cell]) -> Result<(), Fail> {
206        match self.phase {
207            Phase::Rows => {}
208            Phase::BeforeSchema => return Err(Fail::protocol("a row before the schema")),
209            Phase::Done => return Err(self.fail(Fail::protocol("a row after the end"))),
210        }
211        if cells.len() != self.labels.len() {
212            return Err(self.fail(Fail::protocol(format!(
213                "row {} has {} cells; the schema has {} columns",
214                self.rows + 1,
215                cells.len(),
216                self.labels.len()
217            ))));
218        }
219        if let Err(f) = self.check(cells) {
220            return Err(self.fail(f));
221        }
222        for (i, cell) in cells.iter().enumerate() {
223            if i > 0 {
224                self.out.write_str(&self.delimiter)?;
225            }
226            let text: &str = match cell {
227                Cell::Null => &self.options.null_text,
228                Cell::Bool(true) => "true",
229                Cell::Bool(false) => "false",
230                // `check` passed the row: the lexeme is a JSON number and the
231                // value is finite, so this pass only formats, once.
232                Cell::Number {
233                    lexeme: Some(l), ..
234                } => l,
235                Cell::Number { value, .. } => write_value(*value, &mut self.scratch),
236                Cell::String(s) => s,
237                Cell::Missing => match &self.options.missing {
238                    MissingText::Text(t) => t,
239                    // `check` rejected this row already.
240                    MissingText::Error => continue,
241                },
242            };
243            self.emitted = true;
244            write_field(
245                &mut self.out,
246                self.options.quoting,
247                self.options.delimiter,
248                text,
249            )?;
250        }
251        self.emitted = true;
252        self.out.write_str(self.options.newline.as_str())?;
253        self.rows += 1;
254        Ok(())
255    }
256
257    /// Reject a row before any of it is written, so a row is rendered whole
258    /// or not at all and the output stays a sequence of complete records
259    /// whatever the caller does after a failure. Nothing is formatted here;
260    /// `row` formats each number once, after the row has passed.
261    fn check(&self, cells: &[Cell]) -> Result<(), Fail> {
262        for (i, cell) in cells.iter().enumerate() {
263            match cell {
264                Cell::Number { value, lexeme } => {
265                    check_number(*value, lexeme.as_deref()).map_err(|f| {
266                        f.at_path(format!(
267                            "column {:?}, row {}",
268                            self.labels[i],
269                            self.rows + 1
270                        ))
271                    })?;
272                }
273                Cell::Missing if self.options.missing == MissingText::Error => {
274                    return Err(Fail::new(
275                        Code::MissingValue,
276                        format!(
277                            "row {} has no value for column {:?}",
278                            self.rows + 1,
279                            self.labels[i]
280                        ),
281                    ));
282                }
283                _ => {}
284            }
285        }
286        Ok(())
287    }
288
289    fn end(&mut self) -> Result<(), Fail> {
290        match self.phase {
291            Phase::Rows => {}
292            Phase::BeforeSchema => return Err(Fail::protocol("the end before the schema")),
293            Phase::Done => return Err(self.fail(Fail::protocol("a second end"))),
294        }
295        // Done only once the flush has succeeded: a table whose last bytes
296        // never reached the writer is not done, whatever `End` said.
297        self.out.flush()?;
298        self.phase = Phase::Done;
299        Ok(())
300    }
301}
302
303impl<O: TextOut> TableSink for CsvRenderer<O> {
304    fn table_event(&mut self, ev: TableEvent<'_>) -> Result<Flow, Fail> {
305        match ev {
306            TableEvent::Schema(columns) => self.schema(columns)?,
307            TableEvent::Row(cells) => self.row(cells)?,
308            TableEvent::End => self.end()?,
309        }
310        Ok(Flow::Continue)
311    }
312}
313
314/// Write one field: quoted with `"` doubled, or bare when the dialect
315/// allows and the text needs no quoting.
316fn write_field<O: TextOut>(
317    out: &mut O,
318    quoting: Quoting,
319    delimiter: char,
320    text: &str,
321) -> Result<(), Fail> {
322    let quote = match quoting {
323        Quoting::Always => true,
324        Quoting::Minimal => text
325            .chars()
326            .any(|c| c == delimiter || matches!(c, '"' | '\r' | '\n')),
327    };
328    if !quote {
329        return out.write_str(text);
330    }
331    out.write_str("\"")?;
332    let mut rest = text;
333    while let Some(i) = rest.find('"') {
334        // Up to and including the quote, then the quote again: doubled.
335        out.write_str(&rest[..=i])?;
336        out.write_str("\"")?;
337        rest = &rest[i + 1..];
338    }
339    if !rest.is_empty() {
340        out.write_str(rest)?;
341    }
342    out.write_str("\"")
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use crate::text::{StringOut, WriteOut};
349
350    fn cols(labels: &[&str]) -> Vec<PublicColumn> {
351        labels.iter().map(|l| PublicColumn::new(*l)).collect()
352    }
353
354    fn s(text: &str) -> Cell {
355        Cell::String(text.into())
356    }
357
358    fn num(lexeme: &str) -> Cell {
359        Cell::Number {
360            value: lexeme.parse().unwrap_or(0.0),
361            lexeme: Some(lexeme.into()),
362        }
363    }
364
365    /// Render a whole table and give the bytes back.
366    fn render(options: CsvOptions, labels: &[&str], rows: &[Vec<Cell>]) -> Result<String, Fail> {
367        let mut r = CsvRenderer::new(StringOut::new(), options)?;
368        let columns = cols(labels);
369        r.table_event(TableEvent::Schema(&columns))?;
370        for row in rows {
371            r.table_event(TableEvent::Row(row))?;
372        }
373        r.table_event(TableEvent::End)?;
374        Ok(r.into_inner().into_string())
375    }
376
377    fn standard(labels: &[&str], rows: &[Vec<Cell>]) -> String {
378        render(CsvOptions::default(), labels, rows).unwrap()
379    }
380
381    #[test]
382    fn every_field_is_quoted_and_every_record_ends_with_crlf() {
383        assert_eq!(
384            standard(&["name", "age"], &[vec![s("ada"), num("36")]]),
385            "\"name\",\"age\"\r\n\"ada\",\"36\"\r\n"
386        );
387    }
388
389    #[test]
390    fn empty_fields_are_an_empty_quote_pair() {
391        assert_eq!(
392            standard(&["a", "b"], &[vec![s(""), s("")]]),
393            "\"a\",\"b\"\r\n\"\",\"\"\r\n"
394        );
395    }
396
397    #[test]
398    fn commas_in_a_field_are_kept_inside_the_quotes() {
399        assert_eq!(
400            standard(&["a"], &[vec![s("x, y, z")]]),
401            "\"a\"\r\n\"x, y, z\"\r\n"
402        );
403    }
404
405    #[test]
406    fn quotes_in_a_field_are_doubled() {
407        assert_eq!(
408            standard(
409                &["a"],
410                &[vec![s("say \"hi\"")], vec![s("\"")], vec![s("\"\"")]]
411            ),
412            "\"a\"\r\n\"say \"\"hi\"\"\"\r\n\"\"\"\"\r\n\"\"\"\"\"\"\r\n"
413        );
414    }
415
416    #[test]
417    fn cr_and_lf_inside_a_field_are_written_as_they_are() {
418        assert_eq!(
419            standard(&["a"], &[vec![s("line1\r\nline2\nline3\rend")]]),
420            "\"a\"\r\n\"line1\r\nline2\nline3\rend\"\r\n"
421        );
422    }
423
424    #[test]
425    fn unicode_passes_through_unchanged() {
426        assert_eq!(
427            standard(&["名"], &[vec![s("héllo 日本語 🚀")]]),
428            "\"名\"\r\n\"héllo 日本語 🚀\"\r\n"
429        );
430    }
431
432    #[test]
433    fn booleans_write_true_and_false() {
434        assert_eq!(
435            standard(&["a", "b"], &[vec![Cell::Bool(false), Cell::Bool(true)]]),
436            "\"a\",\"b\"\r\n\"false\",\"true\"\r\n"
437        );
438    }
439
440    #[test]
441    fn zero_and_other_values_without_a_lexeme_take_the_shortest_form() {
442        let row = vec![
443            Cell::Number {
444                value: 0.0,
445                lexeme: None,
446            },
447            Cell::Number {
448                value: 50.25,
449                lexeme: None,
450            },
451            Cell::Number {
452                value: 1e20,
453                lexeme: None,
454            },
455            Cell::Number {
456                value: 1e21,
457                lexeme: None,
458            },
459            Cell::Number {
460                value: 1e-300,
461                lexeme: None,
462            },
463        ];
464        assert_eq!(
465            standard(&["z", "b", "big", "bigger", "tiny"], &[row]),
466            "\"z\",\"b\",\"big\",\"bigger\",\"tiny\"\r\n\"0\",\"50.25\",\"100000000000000000000\",\"1e21\",\"1e-300\"\r\n"
467        );
468    }
469
470    #[test]
471    fn big_lexemes_are_written_verbatim() {
472        let row = vec![
473            num("123456789012345678901234567890"),
474            num("0.1000000000000000055511151231257827"),
475            num("-1.5E+308"),
476            num("50.250"),
477        ];
478        assert_eq!(
479            standard(&["a", "b", "c", "d"], &[row]),
480            "\"a\",\"b\",\"c\",\"d\"\r\n\"123456789012345678901234567890\",\"0.1000000000000000055511151231257827\",\"-1.5E+308\",\"50.250\"\r\n"
481        );
482    }
483
484    #[test]
485    fn a_lexeme_that_is_not_a_json_number_is_invalid_number() {
486        for bad in ["1.", "01", "NaN", "0x10", "1_000", ""] {
487            let err = render(CsvOptions::default(), &["a"], &[vec![num(bad)]]).unwrap_err();
488            assert_eq!(err.code, Code::InvalidNumber, "{bad:?}");
489            assert!(err.committed_output, "the header was already written");
490        }
491    }
492
493    #[test]
494    fn nan_and_infinity_are_unrepresentable_with_or_without_a_lexeme() {
495        for v in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
496            let row = vec![Cell::Number {
497                value: v,
498                lexeme: None,
499            }];
500            let err = render(CsvOptions::default(), &["a"], &[row]).unwrap_err();
501            assert_eq!(err.code, Code::TargetValueUnrepresentable);
502        }
503        // The lexeme spells a number, but the value beside it overflowed:
504        // the row is refused whole and nothing of it is written.
505        let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
506        let columns = cols(&["a", "b"]);
507        r.table_event(TableEvent::Schema(&columns)).unwrap();
508        let row = [
509            s("x"),
510            Cell::Number {
511                value: f64::INFINITY,
512                lexeme: Some("1e999".into()),
513            },
514        ];
515        let err = r.table_event(TableEvent::Row(&row)).unwrap_err();
516        assert_eq!(err.code, Code::TargetValueUnrepresentable);
517        assert_eq!(err.path.as_deref(), Some("column \"b\", row 1"));
518        assert_eq!(r.into_inner().as_str(), "\"a\",\"b\"\r\n");
519    }
520
521    #[test]
522    fn null_writes_the_null_text_empty_by_default() {
523        assert_eq!(
524            standard(&["a", "b"], &[vec![Cell::Null, s("x")]]),
525            "\"a\",\"b\"\r\n\"\",\"x\"\r\n"
526        );
527        let options = CsvOptions {
528            null_text: "NULL".into(),
529            ..CsvOptions::default()
530        };
531        assert_eq!(
532            render(options, &["a"], &[vec![Cell::Null]]).unwrap(),
533            "\"a\"\r\n\"NULL\"\r\n"
534        );
535    }
536
537    #[test]
538    fn missing_is_an_error_unless_a_text_is_configured() {
539        let err = render(
540            CsvOptions::default(),
541            &["a", "b"],
542            &[vec![s("x"), Cell::Missing]],
543        )
544        .unwrap_err();
545        assert_eq!(err.code, Code::MissingValue);
546        assert!(err.message.contains("\"b\""));
547        assert!(err.committed_output);
548        let options = CsvOptions {
549            missing: MissingText::Text("N/A".into()),
550            ..CsvOptions::default()
551        };
552        assert_eq!(
553            render(options, &["a", "b"], &[vec![Cell::Missing, s("x")]]).unwrap(),
554            "\"a\",\"b\"\r\n\"N/A\",\"x\"\r\n"
555        );
556        let options = CsvOptions {
557            missing: MissingText::Text("".into()),
558            ..CsvOptions::default()
559        };
560        assert_eq!(
561            render(options, &["a"], &[vec![Cell::Missing]]).unwrap(),
562            "\"a\"\r\n\"\"\r\n"
563        );
564    }
565
566    #[test]
567    fn a_row_that_fails_writes_nothing_so_records_stay_whole() {
568        let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
569        let columns = cols(&["a", "b"]);
570        r.table_event(TableEvent::Schema(&columns)).unwrap();
571        let failing = [
572            (vec![s("x"), Cell::Missing], Code::MissingValue),
573            (vec![s("x"), num("1.")], Code::InvalidNumber),
574            (
575                vec![
576                    s("x"),
577                    Cell::Number {
578                        value: f64::NAN,
579                        lexeme: None,
580                    },
581                ],
582                Code::TargetValueUnrepresentable,
583            ),
584        ];
585        for (row, code) in &failing {
586            let err = r.table_event(TableEvent::Row(row)).unwrap_err();
587            assert_eq!(err.code, *code);
588            assert_eq!(r.out.as_str(), "\"a\",\"b\"\r\n", "{code}");
589        }
590        r.table_event(TableEvent::Row(&[s("y"), s("z")])).unwrap();
591        r.table_event(TableEvent::End).unwrap();
592        assert_eq!(r.rows(), 1);
593        assert_eq!(r.into_inner().as_str(), "\"a\",\"b\"\r\n\"y\",\"z\"\r\n");
594    }
595
596    #[test]
597    fn duplicate_labels_are_allowed() {
598        assert_eq!(
599            standard(&["a", "a"], &[vec![s("1"), s("2")]]),
600            "\"a\",\"a\"\r\n\"1\",\"2\"\r\n"
601        );
602    }
603
604    #[test]
605    fn an_empty_row_sequence_still_writes_the_header() {
606        assert_eq!(standard(&["a", "b"], &[]), "\"a\",\"b\"\r\n");
607    }
608
609    #[test]
610    fn the_header_can_be_turned_off() {
611        let options = CsvOptions {
612            header: false,
613            ..CsvOptions::default()
614        };
615        assert_eq!(
616            render(options.clone(), &["a"], &[vec![s("x")]]).unwrap(),
617            "\"x\"\r\n"
618        );
619        assert_eq!(render(options, &["a"], &[]).unwrap(), "");
620    }
621
622    #[test]
623    fn the_final_record_ends_with_the_newline_too() {
624        let out = standard(&["a"], &[vec![s("1")], vec![s("2")]]);
625        assert!(out.ends_with("\"2\"\r\n"));
626        assert_eq!(out.matches("\r\n").count(), 3);
627    }
628
629    #[test]
630    fn lf_is_a_dialect() {
631        let options = CsvOptions {
632            newline: Newline::Lf,
633            ..CsvOptions::default()
634        };
635        assert_eq!(
636            render(options, &["a"], &[vec![s("x")]]).unwrap(),
637            "\"a\"\n\"x\"\n"
638        );
639    }
640
641    #[test]
642    fn a_tab_delimiter_is_a_dialect() {
643        let options = CsvOptions {
644            delimiter: '\t',
645            ..CsvOptions::default()
646        };
647        assert_eq!(
648            render(options, &["a", "b"], &[vec![s("x,y"), s("z")]]).unwrap(),
649            "\"a\"\t\"b\"\r\n\"x,y\"\t\"z\"\r\n"
650        );
651    }
652
653    #[test]
654    fn minimal_quoting_quotes_only_what_needs_it() {
655        let options = CsvOptions {
656            quoting: Quoting::Minimal,
657            ..CsvOptions::default()
658        };
659        let row = vec![
660            s("plain"),
661            s(""),
662            s("a,b"),
663            s("say \"hi\""),
664            s("x\ny"),
665            s("x\ry"),
666            num("1.50"),
667            Cell::Null,
668        ];
669        assert_eq!(
670            render(
671                options,
672                &["p", "e", "c", "q", "lf", "cr", "n", "nul"],
673                &[row]
674            )
675            .unwrap(),
676            "p,e,c,q,lf,cr,n,nul\r\nplain,,\"a,b\",\"say \"\"hi\"\"\",\"x\ny\",\"x\ry\",1.50,\r\n"
677        );
678    }
679
680    #[test]
681    fn minimal_quoting_quotes_a_field_holding_the_dialects_delimiter() {
682        let options = CsvOptions {
683            quoting: Quoting::Minimal,
684            delimiter: ';',
685            ..CsvOptions::default()
686        };
687        assert_eq!(
688            render(options, &["a", "b"], &[vec![s("x;y"), s("x,y")]]).unwrap(),
689            "a;b\r\n\"x;y\";x,y\r\n"
690        );
691    }
692
693    #[test]
694    fn the_quote_a_line_break_and_nul_cannot_be_the_delimiter() {
695        for bad in ['"', '\r', '\n', '\0'] {
696            let options = CsvOptions {
697                delimiter: bad,
698                ..CsvOptions::default()
699            };
700            let err = CsvRenderer::new(StringOut::new(), options).err().unwrap();
701            assert_eq!(err.code, Code::TargetValueUnrepresentable, "{bad:?}");
702        }
703        for ok in [',', ';', '\t', '|', ' ', 'x', '→'] {
704            let options = CsvOptions {
705                delimiter: ok,
706                ..CsvOptions::default()
707            };
708            assert!(
709                CsvRenderer::new(StringOut::new(), options).is_ok(),
710                "{ok:?}"
711            );
712        }
713    }
714
715    #[test]
716    fn zero_columns_is_unrepresentable() {
717        let err = render(CsvOptions::default(), &[], &[]).unwrap_err();
718        assert_eq!(err.code, Code::TargetValueUnrepresentable);
719        assert!(!err.committed_output);
720    }
721
722    #[test]
723    fn a_row_before_the_schema_is_a_protocol_error() {
724        let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
725        let err = r.table_event(TableEvent::Row(&[s("x")])).unwrap_err();
726        assert_eq!(err.code, Code::ProtocolOrderError);
727        assert!(!err.committed_output);
728    }
729
730    #[test]
731    fn a_second_schema_is_a_protocol_error() {
732        let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
733        let columns = cols(&["a"]);
734        r.table_event(TableEvent::Schema(&columns)).unwrap();
735        let err = r.table_event(TableEvent::Schema(&columns)).unwrap_err();
736        assert_eq!(err.code, Code::ProtocolOrderError);
737        assert!(err.committed_output);
738    }
739
740    #[test]
741    fn a_row_of_the_wrong_width_is_a_protocol_error() {
742        for row in [vec![], vec![s("1")], vec![s("1"), s("2"), s("3")]] {
743            let err = render(
744                CsvOptions::default(),
745                &["a", "b"],
746                std::slice::from_ref(&row),
747            )
748            .unwrap_err();
749            assert_eq!(err.code, Code::ProtocolOrderError, "{row:?}");
750            assert!(err.message.contains("row 1 has"), "{}", err.message);
751        }
752    }
753
754    #[test]
755    fn an_end_before_the_schema_is_a_protocol_error() {
756        let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
757        let err = r.table_event(TableEvent::End).unwrap_err();
758        assert_eq!(err.code, Code::ProtocolOrderError);
759    }
760
761    #[test]
762    fn events_after_the_end_are_protocol_errors() {
763        let mut r = CsvRenderer::new(StringOut::new(), CsvOptions::default()).unwrap();
764        let columns = cols(&["a"]);
765        r.table_event(TableEvent::Schema(&columns)).unwrap();
766        r.table_event(TableEvent::End).unwrap();
767        assert!(r.is_done());
768        for ev in [
769            TableEvent::End,
770            TableEvent::Row(&[s("x")]),
771            TableEvent::Schema(&columns),
772        ] {
773            let err = r.table_event(ev).unwrap_err();
774            assert_eq!(err.code, Code::ProtocolOrderError);
775            assert!(err.committed_output);
776        }
777    }
778
779    /// A writer that takes every byte and refuses to flush.
780    struct NoFlush;
781
782    impl std::io::Write for NoFlush {
783        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
784            Ok(buf.len())
785        }
786
787        fn flush(&mut self) -> std::io::Result<()> {
788            Err(std::io::Error::other("pipe closed"))
789        }
790    }
791
792    #[test]
793    fn a_failed_flush_at_end_leaves_the_renderer_not_done() {
794        let mut r = CsvRenderer::new(WriteOut::new(NoFlush), CsvOptions::default()).unwrap();
795        let columns = cols(&["a"]);
796        r.table_event(TableEvent::Schema(&columns)).unwrap();
797        let err = r.table_event(TableEvent::End).unwrap_err();
798        assert_eq!(err.code, Code::OutputFailed);
799        assert!(!r.is_done());
800    }
801
802    #[test]
803    fn committed_output_means_bytes_that_reached_the_writer() {
804        // Buffered in the WriteOut, not yet written: the failure leaves no
805        // partial output behind, and says so.
806        let mut r = CsvRenderer::new(WriteOut::new(Vec::new()), CsvOptions::default()).unwrap();
807        let columns = cols(&["a"]);
808        r.table_event(TableEvent::Schema(&columns)).unwrap();
809        let err = r.table_event(TableEvent::Schema(&columns)).unwrap_err();
810        assert_eq!(err.code, Code::ProtocolOrderError);
811        assert!(!err.committed_output);
812        assert_eq!(r.out.committed(), 0);
813        assert!(r.into_inner().into_inner().is_empty());
814
815        // Written through: partial output exists.
816        let out = WriteOut::new(Vec::new()).with_budget(0);
817        let mut r = CsvRenderer::new(out, CsvOptions::default()).unwrap();
818        r.table_event(TableEvent::Schema(&columns)).unwrap();
819        let err = r.table_event(TableEvent::Schema(&columns)).unwrap_err();
820        assert!(err.committed_output);
821        assert_eq!(r.into_inner().into_inner(), b"\"a\"\r\n");
822    }
823
824    #[test]
825    fn end_flushes_the_output_and_nothing_else_does() {
826        let out = WriteOut::new(Vec::new());
827        let mut r = CsvRenderer::new(out, CsvOptions::default()).unwrap();
828        let columns = cols(&["a"]);
829        r.table_event(TableEvent::Schema(&columns)).unwrap();
830        r.table_event(TableEvent::Row(&[s("x")])).unwrap();
831        assert_eq!(r.out.committed(), 0);
832        assert_eq!(r.table_event(TableEvent::End).unwrap(), Flow::Continue);
833        assert_eq!(r.rows(), 1);
834        assert_eq!(r.out.committed(), 10);
835        let bytes = r.into_inner().into_inner();
836        assert_eq!(bytes, b"\"a\"\r\n\"x\"\r\n");
837    }
838}