wp_data_fmt/
csv.rs

1use crate::formatter::DataFormat;
2use std::fmt::Write;
3use wp_model_core::model::{DataField, DataRecord, DataType, types::value::ObjectValue};
4
5pub struct Csv {
6    delimiter: char,
7    quote_char: char,
8    escape_char: char,
9}
10
11impl Default for Csv {
12    fn default() -> Self {
13        Self {
14            delimiter: ',',
15            quote_char: '"',
16            escape_char: '"',
17        }
18    }
19}
20
21impl Csv {
22    pub fn new() -> Self {
23        Self::default()
24    }
25    pub fn with_delimiter(mut self, delimiter: char) -> Self {
26        self.delimiter = delimiter;
27        self
28    }
29    pub fn with_quote_char(mut self, quote_char: char) -> Self {
30        self.quote_char = quote_char;
31        self
32    }
33    pub fn with_escape_char(mut self, escape_char: char) -> Self {
34        self.escape_char = escape_char;
35        self
36    }
37
38    fn escape_string(&self, value: &str, output: &mut String) {
39        let needs_quoting = value.contains(self.delimiter)
40            || value.contains('\n')
41            || value.contains('\r')
42            || value.contains(self.quote_char);
43        if needs_quoting {
44            output.push(self.quote_char);
45            for c in value.chars() {
46                if c == self.quote_char {
47                    output.push(self.escape_char);
48                }
49                output.push(c);
50            }
51            output.push(self.quote_char);
52        } else {
53            output.push_str(value);
54        }
55    }
56}
57impl DataFormat for Csv {
58    type Output = String;
59    fn format_null(&self) -> String {
60        "".to_string()
61    }
62    fn format_bool(&self, value: &bool) -> String {
63        if *value { "true" } else { "false" }.to_string()
64    }
65    fn format_string(&self, value: &str) -> String {
66        let mut o = String::new();
67        self.escape_string(value, &mut o);
68        o
69    }
70    fn format_i64(&self, value: &i64) -> String {
71        value.to_string()
72    }
73    fn format_f64(&self, value: &f64) -> String {
74        value.to_string()
75    }
76    fn format_ip(&self, value: &std::net::IpAddr) -> String {
77        self.format_string(&value.to_string())
78    }
79    fn format_datetime(&self, value: &chrono::NaiveDateTime) -> String {
80        self.format_string(&value.to_string())
81    }
82    fn format_object(&self, value: &ObjectValue) -> String {
83        let mut output = String::new();
84        for (i, (k, v)) in value.iter().enumerate() {
85            if i > 0 {
86                output.push_str(", ");
87            }
88            write!(output, "{}:{}", k, self.fmt_value(v.get_value())).unwrap();
89        }
90        output
91    }
92    fn format_array(&self, value: &[DataField]) -> String {
93        let mut output = String::new();
94        self.escape_string(
95            &value
96                .iter()
97                .map(|f| self.format_field(f))
98                .collect::<Vec<_>>()
99                .join(", "),
100            &mut output,
101        );
102        output
103    }
104    fn format_field(&self, field: &DataField) -> String {
105        self.fmt_value(field.get_value())
106    }
107    fn format_record(&self, record: &DataRecord) -> String {
108        let mut output = String::new();
109        let mut first = true;
110        for field in record
111            .items
112            .iter()
113            .filter(|f| *f.get_meta() != DataType::Ignore)
114        {
115            if !first {
116                output.push(self.delimiter);
117            }
118            first = false;
119            output.push_str(&self.format_field(field));
120        }
121        output
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use std::net::IpAddr;
129    use std::str::FromStr;
130
131    #[test]
132    fn test_csv_default() {
133        let csv = Csv::default();
134        assert_eq!(csv.delimiter, ',');
135        assert_eq!(csv.quote_char, '"');
136        assert_eq!(csv.escape_char, '"');
137    }
138
139    #[test]
140    fn test_csv_new() {
141        let csv = Csv::new();
142        assert_eq!(csv.delimiter, ',');
143    }
144
145    #[test]
146    fn test_csv_builder_pattern() {
147        let csv = Csv::new()
148            .with_delimiter(';')
149            .with_quote_char('\'')
150            .with_escape_char('\\');
151        assert_eq!(csv.delimiter, ';');
152        assert_eq!(csv.quote_char, '\'');
153        assert_eq!(csv.escape_char, '\\');
154    }
155
156    #[test]
157    fn test_format_null() {
158        let csv = Csv::default();
159        assert_eq!(csv.format_null(), "");
160    }
161
162    #[test]
163    fn test_format_bool() {
164        let csv = Csv::default();
165        assert_eq!(csv.format_bool(&true), "true");
166        assert_eq!(csv.format_bool(&false), "false");
167    }
168
169    #[test]
170    fn test_format_string_simple() {
171        let csv = Csv::default();
172        assert_eq!(csv.format_string("hello"), "hello");
173        assert_eq!(csv.format_string("world"), "world");
174    }
175
176    #[test]
177    fn test_format_string_with_delimiter() {
178        let csv = Csv::default();
179        // String containing delimiter should be quoted
180        assert_eq!(csv.format_string("hello,world"), "\"hello,world\"");
181    }
182
183    #[test]
184    fn test_format_string_with_newline() {
185        let csv = Csv::default();
186        assert_eq!(csv.format_string("hello\nworld"), "\"hello\nworld\"");
187        assert_eq!(csv.format_string("hello\rworld"), "\"hello\rworld\"");
188    }
189
190    #[test]
191    fn test_format_string_with_quote() {
192        let csv = Csv::default();
193        // Quote char should be escaped by doubling
194        assert_eq!(csv.format_string("say \"hello\""), "\"say \"\"hello\"\"\"");
195    }
196
197    #[test]
198    fn test_format_i64() {
199        let csv = Csv::default();
200        assert_eq!(csv.format_i64(&0), "0");
201        assert_eq!(csv.format_i64(&42), "42");
202        assert_eq!(csv.format_i64(&-100), "-100");
203        assert_eq!(csv.format_i64(&i64::MAX), i64::MAX.to_string());
204    }
205
206    #[test]
207    fn test_format_f64() {
208        let csv = Csv::default();
209        assert_eq!(csv.format_f64(&0.0), "0");
210        assert_eq!(csv.format_f64(&3.24), "3.24");
211        assert_eq!(csv.format_f64(&-2.5), "-2.5");
212    }
213
214    #[test]
215    fn test_format_ip() {
216        let csv = Csv::default();
217        let ipv4 = IpAddr::from_str("192.168.1.1").unwrap();
218        assert_eq!(csv.format_ip(&ipv4), "192.168.1.1");
219
220        let ipv6 = IpAddr::from_str("::1").unwrap();
221        assert_eq!(csv.format_ip(&ipv6), "::1");
222    }
223
224    #[test]
225    fn test_format_datetime() {
226        let csv = Csv::default();
227        let dt = chrono::NaiveDateTime::parse_from_str("2024-01-15 10:30:45", "%Y-%m-%d %H:%M:%S")
228            .unwrap();
229        let result = csv.format_datetime(&dt);
230        assert!(result.contains("2024"));
231        assert!(result.contains("01"));
232        assert!(result.contains("15"));
233    }
234
235    #[test]
236    fn test_format_record() {
237        let csv = Csv::default();
238        let record = DataRecord {
239            items: vec![
240                DataField::from_chars("name", "Alice"),
241                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            items: vec![
253                DataField::from_chars("a", "x"),
254                DataField::from_chars("b", "y"),
255            ],
256        };
257        let result = csv.format_record(&record);
258        assert_eq!(result, "x;y");
259    }
260
261    #[test]
262    fn test_format_record_with_special_chars() {
263        let csv = Csv::default();
264        let record = DataRecord {
265            items: vec![
266                DataField::from_chars("msg", "hello,world"),
267                DataField::from_digit("count", 5),
268            ],
269        };
270        let result = csv.format_record(&record);
271        assert!(result.contains("\"hello,world\""));
272    }
273}