Skip to main content

shard_den_json_extractor/
format.rs

1//! Output formatting
2
3use serde_json::Value;
4use shard_den_core::Result;
5
6/// Output format options
7#[derive(Debug, Clone, Copy, Default)]
8pub enum OutputFormat {
9    #[default]
10    Json,
11    Csv,
12    Text,
13    Yaml,
14}
15
16/// Formatter for extraction results
17#[derive(Debug, Default)]
18pub struct Formatter;
19
20impl Formatter {
21    /// Create a new formatter
22    pub fn new() -> Self {
23        Self
24    }
25
26    /// Format a JSON value to the specified output format
27    pub fn format(&self, value: &Value, format: OutputFormat) -> Result<String> {
28        match format {
29            OutputFormat::Json => {
30                serde_json::to_string_pretty(value).map_err(shard_den_core::ShardDenError::Json)
31            }
32            OutputFormat::Csv => self.format_csv(value),
33            OutputFormat::Text => self.format_text(value),
34            OutputFormat::Yaml => self.format_yaml(value),
35        }
36    }
37
38    fn format_csv(&self, value: &Value) -> Result<String> {
39        // Handle different value types
40        match value {
41            // Array of objects -> CSV table
42            Value::Array(arr) if !arr.is_empty() && arr.iter().all(|v| v.is_object()) => {
43                let mut headers: Vec<String> = Vec::new();
44                for item in arr {
45                    if let Value::Object(obj) = item {
46                        for key in obj.keys() {
47                            if !headers.contains(key) {
48                                headers.push(key.clone());
49                            }
50                        }
51                    }
52                }
53
54                if headers.is_empty() {
55                    return Ok(String::new());
56                }
57
58                // Build CSV
59                let mut csv = headers.join(",") + "\n";
60                for item in arr {
61                    if let Value::Object(obj) = item {
62                        let row: Vec<String> = headers
63                            .iter()
64                            .map(|h| {
65                                obj.get(h)
66                                    .map(|v| self.escape_csv_value(v))
67                                    .unwrap_or_default()
68                            })
69                            .collect();
70                        csv.push_str(&row.join(","));
71                        csv.push('\n');
72                    }
73                }
74                Ok(csv)
75            }
76            // Simple array -> just values
77            Value::Array(arr) => {
78                let values: Vec<String> = arr.iter().map(|v| self.escape_csv_value(v)).collect();
79                Ok(values.join(","))
80            }
81            // Single value
82            _ => Ok(self.escape_csv_value(value)),
83        }
84    }
85
86    fn escape_csv_value(&self, value: &Value) -> String {
87        match value {
88            Value::String(s) => {
89                if s.contains(',') || s.contains('"') || s.contains('\n') {
90                    format!("\"{}\"", s.replace('"', "\"\""))
91                } else {
92                    s.clone()
93                }
94            }
95            Value::Null => "".to_string(),
96            _ => value.to_string(),
97        }
98    }
99
100    fn format_text(&self, value: &Value) -> Result<String> {
101        match value {
102            Value::String(s) => Ok(s.clone()),
103            Value::Array(arr) => {
104                let items: Vec<String> = arr.iter().map(|v| self.value_to_text(v)).collect();
105                Ok(items.join("\n"))
106            }
107            _ => Ok(self.value_to_text(value)),
108        }
109    }
110
111    fn value_to_text(&self, value: &Value) -> String {
112        match value {
113            Value::String(s) => s.clone(),
114            Value::Null => "null".to_string(),
115            Value::Bool(b) => b.to_string(),
116            Value::Number(n) => n.to_string(),
117            Value::Array(arr) => {
118                let items: Vec<String> = arr.iter().map(|v| self.value_to_text(v)).collect();
119                format!("[{}]", items.join(", "))
120            }
121            Value::Object(obj) => {
122                let pairs: Vec<String> = obj
123                    .iter()
124                    .map(|(k, v)| format!("{}: {}", k, self.value_to_text(v)))
125                    .collect();
126                format!("{{{}}}", pairs.join(", "))
127            }
128        }
129    }
130
131    fn format_yaml(&self, value: &Value) -> Result<String> {
132        serde_yaml::to_string(value).map_err(shard_den_core::ShardDenError::Yaml)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use serde_json::json;
140
141    #[test]
142    fn test_format_json() {
143        let formatter = Formatter::new();
144        let value = json!({"key": "value"});
145        let result = formatter.format(&value, OutputFormat::Json);
146        assert!(result.is_ok());
147    }
148
149    #[test]
150    fn test_format_json_pretty() {
151        let formatter = Formatter::new();
152        let value = json!({"a": 1, "b": 2});
153        let result = formatter.format(&value, OutputFormat::Json).unwrap();
154        assert!(result.contains("a"));
155        assert!(result.contains("1"));
156    }
157
158    #[test]
159    fn test_format_text_string() {
160        let formatter = Formatter::new();
161        let value = json!("hello world");
162        let result = formatter.format(&value, OutputFormat::Text).unwrap();
163        assert_eq!(result, "hello world");
164    }
165
166    #[test]
167    fn test_format_text_number() {
168        let formatter = Formatter::new();
169        let value = json!(42);
170        let result = formatter.format(&value, OutputFormat::Text).unwrap();
171        assert_eq!(result, "42");
172    }
173
174    #[test]
175    fn test_format_text_boolean() {
176        let formatter = Formatter::new();
177        let value = json!(true);
178        let result = formatter.format(&value, OutputFormat::Text).unwrap();
179        assert_eq!(result, "true");
180    }
181
182    #[test]
183    fn test_format_text_null() {
184        let formatter = Formatter::new();
185        let value = json!(null);
186        let result = formatter.format(&value, OutputFormat::Text).unwrap();
187        assert_eq!(result, "null");
188    }
189
190    #[test]
191    fn test_format_text_array() {
192        let formatter = Formatter::new();
193        let value = json!([1, 2, 3]);
194        let result = formatter.format(&value, OutputFormat::Text).unwrap();
195        assert!(result.contains("1"));
196    }
197
198    #[test]
199    fn test_format_text_object() {
200        let formatter = Formatter::new();
201        let value = json!({"a": 1});
202        let result = formatter.format(&value, OutputFormat::Text).unwrap();
203        assert!(result.contains("a"));
204    }
205
206    #[test]
207    fn test_format_csv_array_of_objects() {
208        let formatter = Formatter::new();
209        let value = json!([
210            {"id": 1, "name": "alice"},
211            {"id": 2, "name": "bob"}
212        ]);
213        let result = formatter.format(&value, OutputFormat::Csv).unwrap();
214        let expected = "id,name\n1,alice\n2,bob\n";
215        assert_eq!(result, expected);
216    }
217
218    #[test]
219    fn test_format_csv_simple_array() {
220        let formatter = Formatter::new();
221        let value = json!([1, 2, 3]);
222        let result = formatter.format(&value, OutputFormat::Csv).unwrap();
223        assert_eq!(result, "1,2,3");
224    }
225
226    #[test]
227    fn test_format_csv_single_value() {
228        let formatter = Formatter::new();
229        let value = json!("hello");
230        let result = formatter.format(&value, OutputFormat::Csv).unwrap();
231        assert_eq!(result, "hello");
232    }
233
234    #[test]
235    fn test_format_csv_escape_quotes() {
236        let formatter = Formatter::new();
237        let value = json!(["hello,world", "test\"value"]);
238        let result = formatter.format(&value, OutputFormat::Csv).unwrap();
239        assert!(result.contains("\""));
240    }
241
242    #[test]
243    fn test_format_yaml() {
244        let formatter = Formatter::new();
245        let value = json!({"key": "value"});
246        let result = formatter.format(&value, OutputFormat::Yaml);
247        assert!(result.is_ok());
248    }
249
250    #[test]
251    fn test_format_yaml_array() {
252        let formatter = Formatter::new();
253        let value = json!([1, 2, 3]);
254        let result = formatter.format(&value, OutputFormat::Yaml).unwrap();
255        assert!(result.contains("- 1"));
256    }
257
258    #[test]
259    fn test_format_csv_with_null() {
260        let formatter = Formatter::new();
261        // Array with null values
262        let value = json!([{"name": null}, {"name": "test"}]);
263        let result = formatter.format(&value, OutputFormat::Csv);
264        // Should handle null gracefully
265        assert!(result.is_ok());
266    }
267
268    #[test]
269    fn test_format_csv_empty_array() {
270        let formatter = Formatter::new();
271        // Empty array
272        let value = json!([]);
273        let result = formatter.format(&value, OutputFormat::Csv);
274        assert!(result.is_ok());
275    }
276
277    #[test]
278    fn test_value_to_text_string() {
279        let formatter = Formatter::new();
280        // Direct string value
281        let value = json!("hello");
282        let result = formatter.format(&value, OutputFormat::Text);
283        assert!(result.is_ok());
284        assert_eq!(result.unwrap(), "hello");
285    }
286
287    #[test]
288    fn test_value_to_text_nested_array() {
289        let formatter = Formatter::new();
290        // Nested array in text format
291        let value = json!([[1, 2], [3, 4]]);
292        let result = formatter.format(&value, OutputFormat::Text);
293        assert!(result.is_ok());
294    }
295
296    #[test]
297    fn test_format_csv_empty_headers() {
298        let formatter = Formatter::new();
299        // Test with array of objects that have no keys (edge case)
300        let value = json!([{}, {}]);
301        let result = formatter.format(&value, OutputFormat::Csv).unwrap();
302        // Should return empty string when headers are empty
303        assert_eq!(result, "");
304    }
305
306    #[test]
307    fn test_value_to_text_direct_string() {
308        let formatter = Formatter::new();
309        // Direct string value calls value_to_text
310        let value = json!("direct_string");
311        let result = formatter.format(&value, OutputFormat::Text).unwrap();
312        assert_eq!(result, "direct_string");
313    }
314}