table_extractor/writer/
tsv.rs

1use crate::error::Result;
2use crate::{Table, Writer};
3use std::io::Write as IoWrite;
4
5pub struct TsvWriter {
6    delimiter: char,
7}
8
9impl TsvWriter {
10    pub fn new(delimiter: char) -> Self {
11        Self { delimiter }
12    }
13}
14
15impl Default for TsvWriter {
16    fn default() -> Self {
17        Self::new('\t')
18    }
19}
20
21impl Writer for TsvWriter {
22    fn write(&self, table: &Table, output: &mut dyn IoWrite) -> Result<()> {
23        // Validate headers don't contain delimiter to prevent data corruption
24        for header in table.headers() {
25            if header.contains(self.delimiter) {
26                return Err(crate::error::Error::InvalidFormat(
27                    format!(
28                        "Header '{}' contains delimiter character '{}'. Use -o csv for proper escaping.",
29                        header, self.delimiter
30                    )
31                ));
32            }
33        }
34
35        // Write headers
36        writeln!(
37            output,
38            "{}",
39            table.headers().join(&self.delimiter.to_string())
40        )?;
41
42        // Validate and write rows
43        for (idx, row) in table.rows().iter().enumerate() {
44            for cell in row {
45                if cell.contains(self.delimiter) {
46                    return Err(crate::error::Error::InvalidFormat(
47                        format!(
48                            "Row {} contains delimiter character '{}' in data. Use -o csv for proper escaping.",
49                            idx + 1, self.delimiter
50                        )
51                    ));
52                }
53            }
54            writeln!(output, "{}", row.join(&self.delimiter.to_string()))?;
55        }
56
57        Ok(())
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn test_write_tsv() {
67        let table = Table::new(
68            vec!["id".to_string(), "name".to_string()],
69            vec![
70                vec!["1".to_string(), "Alice".to_string()],
71                vec!["2".to_string(), "Bob".to_string()],
72            ],
73        );
74
75        let writer = TsvWriter::default();
76        let mut output = Vec::new();
77        writer.write(&table, &mut output).unwrap();
78
79        let result = String::from_utf8(output).unwrap();
80        assert_eq!(result, "id\tname\n1\tAlice\n2\tBob\n");
81    }
82
83    #[test]
84    fn test_write_custom_delimiter() {
85        let table = Table::new(
86            vec!["id".to_string(), "name".to_string()],
87            vec![vec!["1".to_string(), "Alice".to_string()]],
88        );
89
90        let writer = TsvWriter::new('|');
91        let mut output = Vec::new();
92        writer.write(&table, &mut output).unwrap();
93
94        let result = String::from_utf8(output).unwrap();
95        assert_eq!(result, "id|name\n1|Alice\n");
96    }
97
98    #[test]
99    fn test_reject_tab_in_data() {
100        let table = Table::new(
101            vec!["id".to_string(), "name".to_string()],
102            vec![vec!["1".to_string(), "Alice\tBob".to_string()]],
103        );
104
105        let writer = TsvWriter::default();
106        let mut output = Vec::new();
107        let result = writer.write(&table, &mut output);
108
109        assert!(result.is_err());
110        assert!(result.unwrap_err().to_string().contains("delimiter"));
111    }
112
113    #[test]
114    fn test_reject_custom_delimiter_in_data() {
115        let table = Table::new(
116            vec!["id".to_string(), "name".to_string()],
117            vec![vec!["1".to_string(), "Uses | pipes".to_string()]],
118        );
119
120        let writer = TsvWriter::new('|');
121        let mut output = Vec::new();
122        let result = writer.write(&table, &mut output);
123
124        assert!(result.is_err());
125        let error_msg = result.unwrap_err().to_string();
126        assert!(error_msg.contains("delimiter"));
127        assert!(error_msg.contains("|"));
128    }
129
130    #[test]
131    fn test_reject_delimiter_in_header() {
132        let table = Table::new(
133            vec!["id".to_string(), "name|alias".to_string()],
134            vec![vec!["1".to_string(), "Alice".to_string()]],
135        );
136
137        let writer = TsvWriter::new('|');
138        let mut output = Vec::new();
139        let result = writer.write(&table, &mut output);
140
141        assert!(result.is_err());
142        let error_msg = result.unwrap_err().to_string();
143        assert!(error_msg.contains("Header"));
144        assert!(error_msg.contains("name|alias"));
145    }
146}