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