Skip to main content

wp_data_fmt/
csv.rs

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, "{}:{}", v.get_name(), 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        // String containing delimiter should be quoted
186        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        // Quote char should be escaped by doubling
200        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
284// ============================================================================
285// 新 trait 实现:ValueFormatter + RecordFormatter
286// ============================================================================
287
288#[allow(clippy::items_after_test_module)]
289impl ValueFormatter for Csv {
290    type Output = String;
291
292    fn format_value(&self, value: &wp_model_core::model::Value) -> String {
293        use wp_model_core::model::Value;
294        match value {
295            Value::Null => String::new(),
296            Value::Bool(v) => if *v { "true" } else { "false" }.to_string(),
297            Value::Chars(v) => {
298                let mut o = String::new();
299                self.escape_string(v, &mut o);
300                o
301            }
302            Value::Digit(v) => v.to_string(),
303            Value::Float(v) => v.to_string(),
304            Value::IpAddr(v) => {
305                let mut o = String::new();
306                self.escape_string(&v.to_string(), &mut o);
307                o
308            }
309            Value::Time(v) => {
310                let mut o = String::new();
311                self.escape_string(&v.to_string(), &mut o);
312                o
313            }
314            Value::Obj(v) => {
315                let mut output = String::new();
316                for (i, (_k, field)) in v.iter().enumerate() {
317                    if i > 0 {
318                        output.push_str(", ");
319                    }
320                    write!(
321                        output,
322                        "{}:{}",
323                        field.get_name(),
324                        self.format_value(field.get_value())
325                    )
326                    .unwrap();
327                }
328                output
329            }
330            Value::Array(v) => {
331                let mut output = String::new();
332                self.escape_string(
333                    &v.iter()
334                        .map(|field| self.format_value(field.get_value()))
335                        .collect::<Vec<_>>()
336                        .join(", "),
337                    &mut output,
338                );
339                output
340            }
341            _ => {
342                let mut o = String::new();
343                self.escape_string(&value.to_string(), &mut o);
344                o
345            }
346        }
347    }
348}
349
350impl RecordFormatter for Csv {
351    fn fmt_field(&self, field: &FieldStorage) -> String {
352        self.format_value(field.get_value())
353    }
354
355    fn fmt_record(&self, record: &DataRecord) -> String {
356        let mut output = String::new();
357        let mut first = true;
358        for field in record
359            .items
360            .iter()
361            .filter(|f| *f.get_meta() != DataType::Ignore)
362        {
363            if !first {
364                output.push(self.delimiter);
365            }
366            first = false;
367            output.push_str(&self.fmt_field(field));
368        }
369        output
370    }
371}