Skip to main content

wp_data_fmt/
csv.rs

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        // String containing delimiter should be quoted
179        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        // Quote char should be escaped by doubling
193        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// ============================================================================
331// 新 trait 实现:ValueFormatter + RecordFormatter
332// ============================================================================
333
334#[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}