buup/transformers/
csv_to_json.rs1use crate::{Transform, TransformError, TransformerCategory};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct CsvToJson;
6
7impl Transform for CsvToJson {
8 fn name(&self) -> &'static str {
9 "CSV to JSON"
10 }
11
12 fn id(&self) -> &'static str {
13 "csvtojson"
14 }
15
16 fn description(&self) -> &'static str {
17 "Converts CSV data to JSON format"
18 }
19
20 fn category(&self) -> TransformerCategory {
21 TransformerCategory::Other
22 }
23
24 fn transform(&self, input: &str) -> Result<String, TransformError> {
25 if input.trim().is_empty() {
26 return Ok("[]".to_string());
27 }
28
29 let mut lines = input.lines().collect::<Vec<_>>();
30 if lines.is_empty() {
31 return Ok("[]".to_string());
32 }
33
34 let header = lines.remove(0);
36 let headers = parse_csv_row(header);
37
38 if headers.is_empty() {
39 return Ok("[]".to_string());
40 }
41
42 let mut json = String::from("[");
44 let mut first_row = true;
45
46 for line in lines {
47 if line.trim().is_empty() {
48 continue;
49 }
50
51 let values = parse_csv_row(line);
52 if values.is_empty() {
53 continue;
54 }
55
56 if !first_row {
57 json.push(',');
58 } else {
59 first_row = false;
60 }
61
62 json.push_str("\n {");
64 let mut first_field = true;
65
66 for (i, value) in values.iter().enumerate() {
67 if i >= headers.len() {
68 break;
69 }
70
71 if !first_field {
72 json.push(',');
73 } else {
74 first_field = false;
75 }
76
77 json.push_str(&format!("\n \"{}\":", escape_json_string(&headers[i])));
79
80 if value.trim().is_empty() {
82 json.push_str("null");
83 } else if value == "true"
84 || value == "false"
85 || value == "null"
86 || value.parse::<f64>().is_ok()
87 {
88 json.push_str(value);
90 } else {
91 json.push_str(&format!("\"{}\"", escape_json_string(value)));
93 }
94 }
95
96 json.push_str("\n }");
97 }
98
99 if first_row {
100 return Ok("[]".to_string());
102 }
103
104 json.push_str("\n]");
105 Ok(json)
106 }
107
108 fn default_test_input(&self) -> &'static str {
109 "id,name,value\n1,apple,1.5\n2,banana,0.75"
110 }
111}
112
113fn parse_csv_row(row: &str) -> Vec<String> {
115 let mut fields = Vec::new();
116 let mut current_field = String::new();
117 let mut in_quotes = false;
118 let mut chars = row.chars().peekable();
119
120 while let Some(c) = chars.next() {
121 match c {
122 '"' => {
123 if in_quotes && chars.peek() == Some(&'"') {
124 chars.next(); current_field.push('"');
127 } else {
128 in_quotes = !in_quotes;
130 }
131 }
132 ',' if !in_quotes => {
133 fields.push(current_field);
135 current_field = String::new();
136 }
137 _ => {
138 current_field.push(c);
139 }
140 }
141 }
142
143 fields.push(current_field);
145 fields
146}
147
148fn escape_json_string(s: &str) -> String {
150 let mut result = String::with_capacity(s.len() + 2);
151
152 for c in s.chars() {
153 match c {
154 '"' => result.push_str("\\\""),
155 '\\' => result.push_str("\\\\"),
156 '\n' => result.push_str("\\n"),
157 '\r' => result.push_str("\\r"),
158 '\t' => result.push_str("\\t"),
159 '\u{0008}' => result.push_str("\\b"),
160 '\u{000C}' => result.push_str("\\f"),
161 _ if c.is_control() => {
162 result.push_str(&format!("\\u{:04x}", c as u32));
163 }
164 _ => result.push(c),
165 }
166 }
167
168 result
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_csv_to_json_basic() {
177 let transformer = CsvToJson;
178 let input = transformer.default_test_input(); let expected_default = r#"[
180 {
181 "id":1,
182 "name":"apple",
183 "value":1.5
184 },
185 {
186 "id":2,
187 "name":"banana",
188 "value":0.75
189 }
190]"#;
191 assert_eq!(transformer.transform(input).unwrap(), expected_default);
192
193 let input_complex = "name,age,active\nAlice,30,true\nBob,25,false";
194 let expected_complex = r#"[
195 {
196 "name":"Alice",
197 "age":30,
198 "active":true
199 },
200 {
201 "name":"Bob",
202 "age":25,
203 "active":false
204 }
205]"#;
206 assert_eq!(
207 transformer.transform(input_complex).unwrap(),
208 expected_complex
209 );
210 }
211
212 #[test]
213 fn test_csv_to_json_with_quotes() {
214 let transformer = CsvToJson;
215 let input = r#"name,quote
216Alice,"Hello, world"
217Bob,"Quoted ""text"" here""#;
218 let expected = r#"[
219 {
220 "name":"Alice",
221 "quote":"Hello, world"
222 },
223 {
224 "name":"Bob",
225 "quote":"Quoted \"text\" here"
226 }
227]"#;
228 assert_eq!(transformer.transform(input).unwrap(), expected);
229 }
230
231 #[test]
232 fn test_csv_to_json_empty() {
233 let transformer = CsvToJson;
234 assert_eq!(transformer.transform("").unwrap(), "[]");
235 }
236
237 #[test]
238 fn test_csv_to_json_header_only() {
239 let transformer = CsvToJson;
240 assert_eq!(transformer.transform("name,age").unwrap(), "[]");
241 }
242
243 #[test]
244 fn test_csv_to_json_with_null() {
245 let transformer = CsvToJson;
246 let input = "name,age\nAlice,\nBob,25";
247 let expected = r#"[
248 {
249 "name":"Alice",
250 "age":null
251 },
252 {
253 "name":"Bob",
254 "age":25
255 }
256]"#;
257 assert_eq!(transformer.transform(input).unwrap(), expected);
258 }
259}