1use crate::formatter::DataFormat;
2use std::fmt::Write;
3use wp_model_core::model::{DataField, DataRecord, DataType, types::value::ObjectValue};
4
5pub struct Csv {
6 delimiter: char,
7 quote_char: char,
8 escape_char: char,
9}
10
11impl Default for Csv {
12 fn default() -> Self {
13 Self {
14 delimiter: ',',
15 quote_char: '"',
16 escape_char: '"',
17 }
18 }
19}
20
21impl Csv {
22 pub fn new() -> Self {
23 Self::default()
24 }
25 pub fn with_delimiter(mut self, delimiter: char) -> Self {
26 self.delimiter = delimiter;
27 self
28 }
29 pub fn with_quote_char(mut self, quote_char: char) -> Self {
30 self.quote_char = quote_char;
31 self
32 }
33 pub fn with_escape_char(mut self, escape_char: char) -> Self {
34 self.escape_char = escape_char;
35 self
36 }
37
38 fn escape_string(&self, value: &str, output: &mut String) {
39 let needs_quoting = value.contains(self.delimiter)
40 || value.contains('\n')
41 || value.contains('\r')
42 || value.contains(self.quote_char);
43 if needs_quoting {
44 output.push(self.quote_char);
45 for c in value.chars() {
46 if c == self.quote_char {
47 output.push(self.escape_char);
48 }
49 output.push(c);
50 }
51 output.push(self.quote_char);
52 } else {
53 output.push_str(value);
54 }
55 }
56}
57impl DataFormat for Csv {
58 type Output = String;
59 fn format_null(&self) -> String {
60 "".to_string()
61 }
62 fn format_bool(&self, value: &bool) -> String {
63 if *value { "true" } else { "false" }.to_string()
64 }
65 fn format_string(&self, value: &str) -> String {
66 let mut o = String::new();
67 self.escape_string(value, &mut o);
68 o
69 }
70 fn format_i64(&self, value: &i64) -> String {
71 value.to_string()
72 }
73 fn format_f64(&self, value: &f64) -> String {
74 value.to_string()
75 }
76 fn format_ip(&self, value: &std::net::IpAddr) -> String {
77 self.format_string(&value.to_string())
78 }
79 fn format_datetime(&self, value: &chrono::NaiveDateTime) -> String {
80 self.format_string(&value.to_string())
81 }
82 fn format_object(&self, value: &ObjectValue) -> String {
83 let mut output = String::new();
84 for (i, (k, v)) in value.iter().enumerate() {
85 if i > 0 {
86 output.push_str(", ");
87 }
88 write!(output, "{}:{}", k, self.fmt_value(v.get_value())).unwrap();
89 }
90 output
91 }
92 fn format_array(&self, value: &[DataField]) -> String {
93 let mut output = String::new();
94 self.escape_string(
95 &value
96 .iter()
97 .map(|f| self.format_field(f))
98 .collect::<Vec<_>>()
99 .join(", "),
100 &mut output,
101 );
102 output
103 }
104 fn format_field(&self, field: &DataField) -> String {
105 self.fmt_value(field.get_value())
106 }
107 fn format_record(&self, record: &DataRecord) -> String {
108 let mut output = String::new();
109 let mut first = true;
110 for field in record
111 .items
112 .iter()
113 .filter(|f| *f.get_meta() != DataType::Ignore)
114 {
115 if !first {
116 output.push(self.delimiter);
117 }
118 first = false;
119 output.push_str(&self.format_field(field));
120 }
121 output
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use std::net::IpAddr;
129 use std::str::FromStr;
130
131 #[test]
132 fn test_csv_default() {
133 let csv = Csv::default();
134 assert_eq!(csv.delimiter, ',');
135 assert_eq!(csv.quote_char, '"');
136 assert_eq!(csv.escape_char, '"');
137 }
138
139 #[test]
140 fn test_csv_new() {
141 let csv = Csv::new();
142 assert_eq!(csv.delimiter, ',');
143 }
144
145 #[test]
146 fn test_csv_builder_pattern() {
147 let csv = Csv::new()
148 .with_delimiter(';')
149 .with_quote_char('\'')
150 .with_escape_char('\\');
151 assert_eq!(csv.delimiter, ';');
152 assert_eq!(csv.quote_char, '\'');
153 assert_eq!(csv.escape_char, '\\');
154 }
155
156 #[test]
157 fn test_format_null() {
158 let csv = Csv::default();
159 assert_eq!(csv.format_null(), "");
160 }
161
162 #[test]
163 fn test_format_bool() {
164 let csv = Csv::default();
165 assert_eq!(csv.format_bool(&true), "true");
166 assert_eq!(csv.format_bool(&false), "false");
167 }
168
169 #[test]
170 fn test_format_string_simple() {
171 let csv = Csv::default();
172 assert_eq!(csv.format_string("hello"), "hello");
173 assert_eq!(csv.format_string("world"), "world");
174 }
175
176 #[test]
177 fn test_format_string_with_delimiter() {
178 let csv = Csv::default();
179 assert_eq!(csv.format_string("hello,world"), "\"hello,world\"");
181 }
182
183 #[test]
184 fn test_format_string_with_newline() {
185 let csv = Csv::default();
186 assert_eq!(csv.format_string("hello\nworld"), "\"hello\nworld\"");
187 assert_eq!(csv.format_string("hello\rworld"), "\"hello\rworld\"");
188 }
189
190 #[test]
191 fn test_format_string_with_quote() {
192 let csv = Csv::default();
193 assert_eq!(csv.format_string("say \"hello\""), "\"say \"\"hello\"\"\"");
195 }
196
197 #[test]
198 fn test_format_i64() {
199 let csv = Csv::default();
200 assert_eq!(csv.format_i64(&0), "0");
201 assert_eq!(csv.format_i64(&42), "42");
202 assert_eq!(csv.format_i64(&-100), "-100");
203 assert_eq!(csv.format_i64(&i64::MAX), i64::MAX.to_string());
204 }
205
206 #[test]
207 fn test_format_f64() {
208 let csv = Csv::default();
209 assert_eq!(csv.format_f64(&0.0), "0");
210 assert_eq!(csv.format_f64(&3.24), "3.24");
211 assert_eq!(csv.format_f64(&-2.5), "-2.5");
212 }
213
214 #[test]
215 fn test_format_ip() {
216 let csv = Csv::default();
217 let ipv4 = IpAddr::from_str("192.168.1.1").unwrap();
218 assert_eq!(csv.format_ip(&ipv4), "192.168.1.1");
219
220 let ipv6 = IpAddr::from_str("::1").unwrap();
221 assert_eq!(csv.format_ip(&ipv6), "::1");
222 }
223
224 #[test]
225 fn test_format_datetime() {
226 let csv = Csv::default();
227 let dt = chrono::NaiveDateTime::parse_from_str("2024-01-15 10:30:45", "%Y-%m-%d %H:%M:%S")
228 .unwrap();
229 let result = csv.format_datetime(&dt);
230 assert!(result.contains("2024"));
231 assert!(result.contains("01"));
232 assert!(result.contains("15"));
233 }
234
235 #[test]
236 fn test_format_record() {
237 let csv = Csv::default();
238 let record = DataRecord {
239 items: vec![
240 DataField::from_chars("name", "Alice"),
241 DataField::from_digit("age", 30),
242 ],
243 };
244 let result = csv.format_record(&record);
245 assert_eq!(result, "Alice,30");
246 }
247
248 #[test]
249 fn test_format_record_with_custom_delimiter() {
250 let csv = Csv::new().with_delimiter(';');
251 let record = DataRecord {
252 items: vec![
253 DataField::from_chars("a", "x"),
254 DataField::from_chars("b", "y"),
255 ],
256 };
257 let result = csv.format_record(&record);
258 assert_eq!(result, "x;y");
259 }
260
261 #[test]
262 fn test_format_record_with_special_chars() {
263 let csv = Csv::default();
264 let record = DataRecord {
265 items: vec![
266 DataField::from_chars("msg", "hello,world"),
267 DataField::from_digit("count", 5),
268 ],
269 };
270 let result = csv.format_record(&record);
271 assert!(result.contains("\"hello,world\""));
272 }
273}