1use tabnas_transduce::{Cell, Code, Fail, Flow, PublicColumn, TableEvent, TableSink};
18
19use crate::number::{check_number, write_value};
20use crate::text::TextOut;
21
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub enum Newline {
25 Lf,
26 #[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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub enum Quoting {
43 #[default]
45 Always,
46 Minimal,
50}
51
52#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub enum MissingText {
55 #[default]
58 Error,
59 Text(Box<str>),
61}
62
63#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct CsvOptions {
66 pub delimiter: char,
70 pub newline: Newline,
71 pub header: bool,
73 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
99pub 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 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 pub fn rows(&self) -> u64 {
150 self.rows
151 }
152
153 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 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 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 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 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 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
314fn 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 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 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 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 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 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 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}