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