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::{
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        // String containing delimiter should be quoted
188        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        // Quote char should be escaped by doubling
202        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// ============================================================================
287// 新 trait 实现:ValueFormatter + RecordFormatter
288// ============================================================================
289
290#[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}