1#[allow(deprecated)]
2use crate::formatter::DataFormat;
3use crate::formatter::{RecordFormatter, ValueFormatter};
4use std::fmt::Write;
5use wp_model_core::model::{
6 DataRecord, DataType, FieldStorage, data::record::RecordItem, types::value::ObjectValue,
7};
8
9pub struct Csv {
10 delimiter: char,
11 quote_char: char,
12 escape_char: char,
13}
14
15impl Default for Csv {
16 fn default() -> Self {
17 Self {
18 delimiter: ',',
19 quote_char: '"',
20 escape_char: '"',
21 }
22 }
23}
24
25impl Csv {
26 pub fn new() -> Self {
27 Self::default()
28 }
29 pub fn with_delimiter(mut self, delimiter: char) -> Self {
30 self.delimiter = delimiter;
31 self
32 }
33 pub fn with_quote_char(mut self, quote_char: char) -> Self {
34 self.quote_char = quote_char;
35 self
36 }
37 pub fn with_escape_char(mut self, escape_char: char) -> Self {
38 self.escape_char = escape_char;
39 self
40 }
41
42 fn escape_string(&self, value: &str, output: &mut String) {
43 let needs_quoting = value.contains(self.delimiter)
44 || value.contains('\n')
45 || value.contains('\r')
46 || value.contains(self.quote_char);
47 if needs_quoting {
48 output.push(self.quote_char);
49 for c in value.chars() {
50 if c == self.quote_char {
51 output.push(self.escape_char);
52 }
53 output.push(c);
54 }
55 output.push(self.quote_char);
56 } else {
57 output.push_str(value);
58 }
59 }
60}
61
62#[allow(deprecated)]
63impl DataFormat for Csv {
64 type Output = String;
65 fn format_null(&self) -> String {
66 "".to_string()
67 }
68 fn format_bool(&self, value: &bool) -> String {
69 if *value { "true" } else { "false" }.to_string()
70 }
71 fn format_string(&self, value: &str) -> String {
72 let mut o = String::new();
73 self.escape_string(value, &mut o);
74 o
75 }
76 fn format_i64(&self, value: &i64) -> String {
77 value.to_string()
78 }
79 fn format_f64(&self, value: &f64) -> String {
80 value.to_string()
81 }
82 fn format_ip(&self, value: &std::net::IpAddr) -> String {
83 self.format_string(&value.to_string())
84 }
85 fn format_datetime(&self, value: &chrono::NaiveDateTime) -> String {
86 self.format_string(&value.to_string())
87 }
88 fn format_object(&self, value: &ObjectValue) -> String {
89 let mut output = String::new();
90 for (i, (k, v)) in value.iter().enumerate() {
91 if i > 0 {
92 output.push_str(", ");
93 }
94 write!(output, "{}:{}", k, self.fmt_value(v.get_value())).unwrap();
95 }
96 output
97 }
98 fn format_array(&self, value: &[FieldStorage]) -> String {
99 let mut output = String::new();
100 self.escape_string(
101 &value
102 .iter()
103 .map(|f| self.fmt_value(f.get_value()))
104 .collect::<Vec<_>>()
105 .join(", "),
106 &mut output,
107 );
108 output
109 }
110 fn format_field(&self, field: &FieldStorage) -> String {
111 self.fmt_value(field.get_value())
112 }
113 fn format_record(&self, record: &DataRecord) -> String {
114 let mut output = String::new();
115 let mut first = true;
116 for field in record
117 .items
118 .iter()
119 .filter(|f| *f.get_meta() != DataType::Ignore)
120 {
121 if !first {
122 output.push(self.delimiter);
123 }
124 first = false;
125 output.push_str(&self.format_field(field));
126 }
127 output
128 }
129}
130
131#[cfg(test)]
132#[allow(deprecated)]
133mod tests {
134 use super::*;
135 use std::net::IpAddr;
136 use std::str::FromStr;
137 use wp_model_core::model::DataField;
138
139 #[test]
140 fn test_csv_default() {
141 let csv = Csv::default();
142 assert_eq!(csv.delimiter, ',');
143 assert_eq!(csv.quote_char, '"');
144 assert_eq!(csv.escape_char, '"');
145 }
146
147 #[test]
148 fn test_csv_new() {
149 let csv = Csv::new();
150 assert_eq!(csv.delimiter, ',');
151 }
152
153 #[test]
154 fn test_csv_builder_pattern() {
155 let csv = Csv::new()
156 .with_delimiter(';')
157 .with_quote_char('\'')
158 .with_escape_char('\\');
159 assert_eq!(csv.delimiter, ';');
160 assert_eq!(csv.quote_char, '\'');
161 assert_eq!(csv.escape_char, '\\');
162 }
163
164 #[test]
165 fn test_format_null() {
166 let csv = Csv::default();
167 assert_eq!(csv.format_null(), "");
168 }
169
170 #[test]
171 fn test_format_bool() {
172 let csv = Csv::default();
173 assert_eq!(csv.format_bool(&true), "true");
174 assert_eq!(csv.format_bool(&false), "false");
175 }
176
177 #[test]
178 fn test_format_string_simple() {
179 let csv = Csv::default();
180 assert_eq!(csv.format_string("hello"), "hello");
181 assert_eq!(csv.format_string("world"), "world");
182 }
183
184 #[test]
185 fn test_format_string_with_delimiter() {
186 let csv = Csv::default();
187 assert_eq!(csv.format_string("hello,world"), "\"hello,world\"");
189 }
190
191 #[test]
192 fn test_format_string_with_newline() {
193 let csv = Csv::default();
194 assert_eq!(csv.format_string("hello\nworld"), "\"hello\nworld\"");
195 assert_eq!(csv.format_string("hello\rworld"), "\"hello\rworld\"");
196 }
197
198 #[test]
199 fn test_format_string_with_quote() {
200 let csv = Csv::default();
201 assert_eq!(csv.format_string("say \"hello\""), "\"say \"\"hello\"\"\"");
203 }
204
205 #[test]
206 fn test_format_i64() {
207 let csv = Csv::default();
208 assert_eq!(csv.format_i64(&0), "0");
209 assert_eq!(csv.format_i64(&42), "42");
210 assert_eq!(csv.format_i64(&-100), "-100");
211 assert_eq!(csv.format_i64(&i64::MAX), i64::MAX.to_string());
212 }
213
214 #[test]
215 fn test_format_f64() {
216 let csv = Csv::default();
217 assert_eq!(csv.format_f64(&0.0), "0");
218 assert_eq!(csv.format_f64(&3.24), "3.24");
219 assert_eq!(csv.format_f64(&-2.5), "-2.5");
220 }
221
222 #[test]
223 fn test_format_ip() {
224 let csv = Csv::default();
225 let ipv4 = IpAddr::from_str("192.168.1.1").unwrap();
226 assert_eq!(csv.format_ip(&ipv4), "192.168.1.1");
227
228 let ipv6 = IpAddr::from_str("::1").unwrap();
229 assert_eq!(csv.format_ip(&ipv6), "::1");
230 }
231
232 #[test]
233 fn test_format_datetime() {
234 let csv = Csv::default();
235 let dt = chrono::NaiveDateTime::parse_from_str("2024-01-15 10:30:45", "%Y-%m-%d %H:%M:%S")
236 .unwrap();
237 let result = csv.format_datetime(&dt);
238 assert!(result.contains("2024"));
239 assert!(result.contains("01"));
240 assert!(result.contains("15"));
241 }
242
243 #[test]
244 fn test_format_record() {
245 let csv = Csv::default();
246 let record = DataRecord {
247 id: Default::default(),
248 items: vec![
249 FieldStorage::Owned(DataField::from_chars("name", "Alice")),
250 FieldStorage::Owned(DataField::from_digit("age", 30)),
251 ],
252 };
253 let result = csv.format_record(&record);
254 assert_eq!(result, "Alice,30");
255 }
256
257 #[test]
258 fn test_format_record_with_custom_delimiter() {
259 let csv = Csv::new().with_delimiter(';');
260 let record = DataRecord {
261 id: Default::default(),
262 items: vec![
263 FieldStorage::Owned(DataField::from_chars("a", "x")),
264 FieldStorage::Owned(DataField::from_chars("b", "y")),
265 ],
266 };
267 let result = csv.format_record(&record);
268 assert_eq!(result, "x;y");
269 }
270
271 #[test]
272 fn test_format_record_with_special_chars() {
273 let csv = Csv::default();
274 let record = DataRecord {
275 id: Default::default(),
276 items: vec![
277 FieldStorage::Owned(DataField::from_chars("msg", "hello,world")),
278 FieldStorage::Owned(DataField::from_digit("count", 5)),
279 ],
280 };
281 let result = csv.format_record(&record);
282 assert!(result.contains("\"hello,world\""));
283 }
284}
285
286#[allow(clippy::items_after_test_module)]
291impl ValueFormatter for Csv {
292 type Output = String;
293
294 fn format_value(&self, value: &wp_model_core::model::Value) -> String {
295 use wp_model_core::model::Value;
296 match value {
297 Value::Null => String::new(),
298 Value::Bool(v) => if *v { "true" } else { "false" }.to_string(),
299 Value::Chars(v) => {
300 let mut o = String::new();
301 self.escape_string(v, &mut o);
302 o
303 }
304 Value::Digit(v) => v.to_string(),
305 Value::Float(v) => v.to_string(),
306 Value::IpAddr(v) => {
307 let mut o = String::new();
308 self.escape_string(&v.to_string(), &mut o);
309 o
310 }
311 Value::Time(v) => {
312 let mut o = String::new();
313 self.escape_string(&v.to_string(), &mut o);
314 o
315 }
316 Value::Obj(v) => {
317 let mut output = String::new();
318 for (i, (k, field)) in v.iter().enumerate() {
319 if i > 0 {
320 output.push_str(", ");
321 }
322 write!(output, "{}:{}", k, self.format_value(field.get_value())).unwrap();
323 }
324 output
325 }
326 Value::Array(v) => {
327 let mut output = String::new();
328 self.escape_string(
329 &v.iter()
330 .map(|field| self.format_value(field.get_value()))
331 .collect::<Vec<_>>()
332 .join(", "),
333 &mut output,
334 );
335 output
336 }
337 _ => {
338 let mut o = String::new();
339 self.escape_string(&value.to_string(), &mut o);
340 o
341 }
342 }
343 }
344}
345
346impl RecordFormatter for Csv {
347 fn fmt_field(&self, field: &FieldStorage) -> String {
348 self.format_value(field.get_value())
349 }
350
351 fn fmt_record(&self, record: &DataRecord) -> String {
352 let mut output = String::new();
353 let mut first = true;
354 for field in record
355 .items
356 .iter()
357 .filter(|f| *f.get_meta() != DataType::Ignore)
358 {
359 if !first {
360 output.push(self.delimiter);
361 }
362 first = false;
363 output.push_str(&self.fmt_field(field));
364 }
365 output
366 }
367}