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