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 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 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#[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}