Skip to main content

reliakit_csv/
writer.rs

1//! Deterministic CSV writing.
2
3use alloc::string::String;
4
5/// Builds CSV text one record at a time.
6///
7/// Output is deterministic: a field is quoted only when it contains a delimiter
8/// (`,`), a quote (`"`), or a line break (`\r`/`\n`); an embedded quote is
9/// doubled; and every record is terminated with `\r\n` (per RFC 4180).
10/// In-memory writing cannot fail.
11///
12/// ```
13/// use reliakit_csv::CsvWriter;
14///
15/// let mut writer = CsvWriter::new();
16/// writer.write_record(["id", "note"]);
17/// writer.write_record(["1", "has \"quotes\""]);
18/// assert_eq!(writer.into_string(), "id,note\r\n1,\"has \"\"quotes\"\"\"\r\n");
19/// ```
20#[derive(Debug, Clone, Default)]
21pub struct CsvWriter {
22    out: String,
23}
24
25impl CsvWriter {
26    /// Creates an empty writer.
27    pub const fn new() -> Self {
28        Self { out: String::new() }
29    }
30
31    /// Writes one record from an iterator of field values, terminated by `\r\n`.
32    pub fn write_record<I, S>(&mut self, fields: I)
33    where
34        I: IntoIterator<Item = S>,
35        S: AsRef<str>,
36    {
37        let mut first = true;
38        for field in fields {
39            if !first {
40                self.out.push(',');
41            }
42            first = false;
43            write_field(&mut self.out, field.as_ref());
44        }
45        self.out.push_str("\r\n");
46    }
47
48    /// Returns the number of bytes written so far.
49    pub fn len(&self) -> usize {
50        self.out.len()
51    }
52
53    /// Returns `true` if no records have been written.
54    pub fn is_empty(&self) -> bool {
55        self.out.is_empty()
56    }
57
58    /// Returns a borrowed view of the CSV written so far.
59    pub fn as_str(&self) -> &str {
60        &self.out
61    }
62
63    /// Consumes the writer and returns the CSV text.
64    pub fn into_string(self) -> String {
65        self.out
66    }
67}
68
69/// Appends a single field to `out`, quoting and escaping only as required.
70fn write_field(out: &mut String, field: &str) {
71    if needs_quoting(field) {
72        out.push('"');
73        for c in field.chars() {
74            if c == '"' {
75                out.push('"');
76            }
77            out.push(c);
78        }
79        out.push('"');
80    } else {
81        out.push_str(field);
82    }
83}
84
85/// A field needs quoting if it contains a delimiter, a quote, or a line break.
86fn needs_quoting(field: &str) -> bool {
87    field
88        .bytes()
89        .any(|b| matches!(b, b',' | b'"' | b'\r' | b'\n'))
90}