table_extractor/writer/
tsv.rs1use 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 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 writeln!(
37 output,
38 "{}",
39 table.headers().join(&self.delimiter.to_string())
40 )?;
41
42 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}