1use comp_cat_rs::effect::io::Io;
7use comp_cat_rs::effect::resource::Resource;
8
9use crate::error::CsvError;
10use crate::row::Row;
11
12#[derive(Debug, Clone, Copy)]
14pub struct WriterConfig {
15 delimiter: u8,
16 _has_headers: bool,
17}
18
19impl WriterConfig {
20 #[must_use]
22 pub fn new() -> Self {
23 Self {
24 delimiter: b',',
25 _has_headers: true,
26 }
27 }
28
29 #[must_use]
31 pub fn delimiter(self, d: u8) -> Self {
32 Self { delimiter: d, ..self }
33 }
34
35 #[must_use]
37 pub fn has_headers(self, v: bool) -> Self {
38 Self { _has_headers: v, ..self }
39 }
40}
41
42impl Default for WriterConfig {
43 fn default() -> Self { Self::new() }
44}
45
46pub fn write_all(
53 path: impl Into<String>,
54 config: WriterConfig,
55 headers: Option<Vec<String>>,
56 rows: Vec<Row>,
57) -> Io<CsvError, ()> {
58 let path: String = path.into();
59 Io::suspend(move || {
60 let file = std::fs::File::create(&path)?;
61 #[allow(unused_mut)]
62 let mut builder = csv::WriterBuilder::new();
63 builder.delimiter(config.delimiter);
64 #[allow(unused_mut)]
65 let mut writer = builder.from_writer(file);
66
67 headers.iter().flatten().try_for_each(|_| -> Result<(), CsvError> {
68 Ok(())
70 })?;
71
72 headers.map(|h| -> Result<(), CsvError> {
74 writer.write_record(&h).map_err(CsvError::from)
75 }).transpose()?;
76
77 rows.iter().try_for_each(|row| -> Result<(), CsvError> {
79 writer.write_record(row.fields()).map_err(CsvError::from)
80 })?;
81
82 writer.flush().map_err(CsvError::from)
83 })
84}
85
86#[must_use]
92pub fn to_string(
93 config: WriterConfig,
94 headers: Option<Vec<String>>,
95 rows: Vec<Row>,
96) -> Io<CsvError, String> {
97 Io::suspend(move || {
98 #[allow(unused_mut)]
99 let mut builder = csv::WriterBuilder::new();
100 builder.delimiter(config.delimiter);
101 #[allow(unused_mut)]
102 let mut writer = builder.from_writer(Vec::new());
103
104 headers.map(|h| -> Result<(), CsvError> {
105 writer.write_record(&h).map_err(CsvError::from)
106 }).transpose()?;
107
108 rows.iter().try_for_each(|row| -> Result<(), CsvError> {
109 writer.write_record(row.fields()).map_err(CsvError::from)
110 })?;
111
112 let bytes = writer.into_inner()
113 .map_err(|e| CsvError::Io(e.into_error()))?;
114 String::from_utf8(bytes)
115 .map_err(|e| CsvError::Deserialize(e.to_string()))
116 })
117}
118
119pub fn writer_resource(
123 path: impl Into<String>,
124 config: WriterConfig,
125 headers: Option<Vec<String>>,
126 rows: Vec<Row>,
127) -> Resource<CsvError, ()> {
128 let path: String = path.into();
129 Resource::make(
130 move || write_all(path, config, headers, rows),
131 |()| Io::pure(()),
132 )
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn sample_rows() -> Vec<Row> {
140 vec![
141 Row::from_record(csv::StringRecord::from(vec!["alice", "30"])),
142 Row::from_record(csv::StringRecord::from(vec!["bob", "25"])),
143 ]
144 }
145
146 #[test]
147 fn to_string_writes_csv() -> Result<(), CsvError> {
148 let output = to_string(
149 WriterConfig::new(),
150 Some(vec!["name".into(), "age".into()]),
151 sample_rows(),
152 ).run()?;
153 assert!(output.contains("name,age"));
154 assert!(output.contains("alice,30"));
155 assert!(output.contains("bob,25"));
156 Ok(())
157 }
158
159 #[test]
160 fn to_string_without_headers() -> Result<(), CsvError> {
161 let output = to_string(
162 WriterConfig::new(),
163 None,
164 sample_rows(),
165 ).run()?;
166 assert!(!output.contains("name"));
167 assert!(output.contains("alice,30"));
168 Ok(())
169 }
170
171 #[test]
172 fn to_string_with_tab_delimiter() -> Result<(), CsvError> {
173 let output = to_string(
174 WriterConfig::new().delimiter(b'\t'),
175 None,
176 sample_rows(),
177 ).run()?;
178 assert!(output.contains("alice\t30"));
179 Ok(())
180 }
181}