ddb_core/output/
formatter.rs1use crate::engine::Row;
3use crate::error::Result;
4use prettytable::{Cell, Row as PrettyRow, Table};
5
6#[derive(Debug, Clone, Copy)]
7pub enum OutputFormat {
8 Table,
9 Json,
10 Yaml,
11 Csv,
12 Xml,
13}
14
15impl OutputFormat {
16 pub fn from_str(s: &str) -> Self {
17 match s.to_lowercase().as_str() {
18 "json" => OutputFormat::Json,
19 "yaml" => OutputFormat::Yaml,
20 "csv" => OutputFormat::Csv,
21 "xml" => OutputFormat::Xml,
22 _ => OutputFormat::Table,
23 }
24 }
25}
26
27pub fn format_results(rows: &[Row], format: OutputFormat) -> Result<String> {
29 if rows.is_empty() {
30 return Ok("No results".to_string());
31 }
32
33 match format {
34 OutputFormat::Table => format_table(rows),
35 OutputFormat::Json => format_json(rows),
36 OutputFormat::Yaml => format_yaml(rows),
37 OutputFormat::Csv => format_csv(rows),
38 OutputFormat::Xml => format_xml(rows),
39 }
40}
41
42fn format_table(rows: &[Row]) -> Result<String> {
44 if rows.is_empty() {
45 return Ok("No results".to_string());
46 }
47
48 let mut table = Table::new();
49
50 let columns: Vec<String> = rows[0].column_names();
52
53 let header: Vec<Cell> = columns.iter().map(|c| Cell::new(c)).collect();
55 table.add_row(PrettyRow::new(header));
56
57 for row in rows {
59 let values = row.values(&columns);
60 let cells: Vec<Cell> = values.iter().map(|v| Cell::new(&v.to_string())).collect();
61 table.add_row(PrettyRow::new(cells));
62 }
63
64 Ok(table.to_string())
65}
66
67fn format_json(rows: &[Row]) -> Result<String> {
69 let json_rows: Vec<serde_json::Value> = rows
70 .iter()
71 .map(|row| {
72 let mut map = serde_json::Map::new();
73 for (key, value) in row.as_map() {
74 map.insert(key.clone(), value_to_json(value));
75 }
76 serde_json::Value::Object(map)
77 })
78 .collect();
79
80 serde_json::to_string_pretty(&json_rows)
81 .map_err(|e| crate::error::DdbError::SerializationError(e.to_string()))
82}
83
84fn format_yaml(rows: &[Row]) -> Result<String> {
86 let yaml_rows: Vec<serde_yaml::Value> = rows
87 .iter()
88 .map(|row| {
89 let mut map = serde_yaml::Mapping::new();
90 for (key, value) in row.as_map() {
91 map.insert(
92 serde_yaml::Value::String(key.clone()),
93 value_to_yaml(value),
94 );
95 }
96 serde_yaml::Value::Mapping(map)
97 })
98 .collect();
99
100 serde_yaml::to_string(&yaml_rows)
101 .map_err(|e| crate::error::DdbError::SerializationError(e.to_string()))
102}
103
104fn format_csv(rows: &[Row]) -> Result<String> {
106 if rows.is_empty() {
107 return Ok(String::new());
108 }
109
110 let mut output = String::new();
111 let columns: Vec<String> = rows[0].column_names();
112
113 output.push_str(&columns.join(","));
115 output.push('\n');
116
117 for row in rows {
119 let values: Vec<String> = row
120 .values(&columns)
121 .iter()
122 .map(|v| escape_csv(&v.to_string()))
123 .collect();
124 output.push_str(&values.join(","));
125 output.push('\n');
126 }
127
128 Ok(output)
129}
130
131fn format_xml(rows: &[Row]) -> Result<String> {
133 if rows.is_empty() {
134 return Ok("<results></results>".to_string());
135 }
136
137 let mut output = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<results>\n");
138
139 for row in rows {
140 output.push_str(" <row>\n");
141 for (key, value) in row.as_map() {
142 let safe_key = escape_xml_tag(key);
144 let value_str = escape_xml(&value_to_string(value));
145 output.push_str(&format!(" <{}>{}</{}>\n", safe_key, value_str, safe_key));
146 }
147 output.push_str(" </row>\n");
148 }
149
150 output.push_str("</results>");
151 Ok(output)
152}
153
154fn escape_xml_tag(s: &str) -> String {
156 s.chars()
157 .map(|c| if c.is_alphanumeric() || c == '_' { c } else { '_' })
158 .collect()
159}
160
161fn escape_xml(s: &str) -> String {
163 s.replace('&', "&")
164 .replace('<', "<")
165 .replace('>', ">")
166 .replace('"', """)
167 .replace('\'', "'")
168}
169
170fn value_to_string(value: &crate::functions::Value) -> String {
172 use crate::functions::Value;
173
174 match value {
175 Value::Null => String::new(),
176 Value::Boolean(b) => b.to_string(),
177 Value::Integer(i) => i.to_string(),
178 Value::Float(f) => f.to_string(),
179 Value::String(s) => s.clone(),
180 Value::Date(d) => d.format("%Y-%m-%d").to_string(),
181 Value::DateTime(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
182 Value::Time(t) => t.format("%H:%M:%S").to_string(),
183 }
184}
185
186fn escape_csv(s: &str) -> String {
188 if s.contains(',') || s.contains('"') || s.contains('\n') {
189 format!("\"{}\"", s.replace('"', "\"\""))
190 } else {
191 s.to_string()
192 }
193}
194
195fn value_to_json(value: &crate::functions::Value) -> serde_json::Value {
197 use crate::functions::Value;
198
199 match value {
200 Value::Null => serde_json::Value::Null,
201 Value::Boolean(b) => serde_json::Value::Bool(*b),
202 Value::Integer(i) => serde_json::Value::Number((*i).into()),
203 Value::Float(f) => serde_json::Number::from_f64(*f)
204 .map(serde_json::Value::Number)
205 .unwrap_or(serde_json::Value::Null),
206 Value::String(s) => serde_json::Value::String(s.clone()),
207 Value::Date(d) => serde_json::Value::String(d.format("%Y-%m-%d").to_string()),
208 Value::DateTime(dt) => serde_json::Value::String(dt.format("%Y-%m-%d %H:%M:%S").to_string()),
209 Value::Time(t) => serde_json::Value::String(t.format("%H:%M:%S").to_string()),
210 }
211}
212
213fn value_to_yaml(value: &crate::functions::Value) -> serde_yaml::Value {
215 use crate::functions::Value;
216
217 match value {
218 Value::Null => serde_yaml::Value::Null,
219 Value::Boolean(b) => serde_yaml::Value::Bool(*b),
220 Value::Integer(i) => serde_yaml::Value::Number((*i).into()),
221 Value::Float(f) => serde_yaml::Value::Number(serde_yaml::Number::from(*f)),
222 Value::String(s) => serde_yaml::Value::String(s.clone()),
223 Value::Date(d) => serde_yaml::Value::String(d.format("%Y-%m-%d").to_string()),
224 Value::DateTime(dt) => serde_yaml::Value::String(dt.format("%Y-%m-%d %H:%M:%S").to_string()),
225 Value::Time(t) => serde_yaml::Value::String(t.format("%H:%M:%S").to_string()),
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::functions::Value;
233
234 fn create_test_rows() -> Vec<Row> {
235 let columns = vec!["id".to_string(), "name".to_string(), "age".to_string()];
236 let values1 = vec![
237 Value::Integer(1),
238 Value::String("Alice".to_string()),
239 Value::Integer(30),
240 ];
241 let values2 = vec![
242 Value::Integer(2),
243 Value::String("Bob".to_string()),
244 Value::Integer(25),
245 ];
246
247 vec![
248 Row::from_values(&columns, values1),
249 Row::from_values(&columns, values2),
250 ]
251 }
252
253 #[test]
254 fn test_format_csv() {
255 let rows = create_test_rows();
256 let output = format_csv(&rows).unwrap();
257
258 assert!(output.contains("id"));
260 assert!(output.contains("name"));
261 assert!(output.contains("age"));
262 assert!(output.contains("Alice"));
264 assert!(output.contains("Bob"));
265 assert!(output.contains("30"));
266 assert!(output.contains("25"));
267 }
268
269 #[test]
270 fn test_format_json() {
271 let rows = create_test_rows();
272 let output = format_json(&rows).unwrap();
273
274 assert!(output.contains("\"id\""));
275 assert!(output.contains("\"name\""));
276 assert!(output.contains("Alice"));
277 assert!(output.contains("Bob"));
278 }
279
280 #[test]
281 fn test_format_yaml() {
282 let rows = create_test_rows();
283 let output = format_yaml(&rows).unwrap();
284
285 assert!(output.contains("id:"));
286 assert!(output.contains("name:"));
287 assert!(output.contains("Alice"));
288 assert!(output.contains("Bob"));
289 }
290
291 #[test]
292 fn test_format_xml() {
293 let rows = create_test_rows();
294 let output = format_xml(&rows).unwrap();
295
296 assert!(output.contains("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
297 assert!(output.contains("<results>"));
298 assert!(output.contains("<row>"));
299 assert!(output.contains("<id>"));
300 assert!(output.contains("<name>"));
301 assert!(output.contains("Alice"));
302 assert!(output.contains("Bob"));
303 assert!(output.contains("</results>"));
304 }
305}