Skip to main content

alopex_cli/output/
jsonl.rs

1//! JSON Lines formatter
2//!
3//! Outputs data as JSON Lines (one JSON object per line).
4//! Supports streaming output.
5
6use std::io::Write;
7
8use crate::error::Result;
9use crate::models::{Column, Row, Value};
10
11use super::formatter::Formatter;
12
13/// JSON Lines formatter.
14///
15/// Outputs each row as a single-line JSON object.
16/// Format: `{"col1": val1, "col2": val2, ...}\n`
17pub struct JsonlFormatter {
18    /// Column names (set on write_header)
19    columns: Vec<String>,
20}
21
22impl JsonlFormatter {
23    /// Create a new JSON Lines formatter.
24    pub fn new() -> Self {
25        Self {
26            columns: Vec::new(),
27        }
28    }
29}
30
31impl Default for JsonlFormatter {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl Formatter for JsonlFormatter {
38    fn write_header(&mut self, _writer: &mut dyn Write, columns: &[Column]) -> Result<()> {
39        // JSON Lines doesn't have a header, just store column names
40        self.columns = columns.iter().map(|c| c.name.clone()).collect();
41        Ok(())
42    }
43
44    fn write_row(&mut self, writer: &mut dyn Write, row: &Row) -> Result<()> {
45        // Build a JSON object from column names and values
46        let mut obj = serde_json::Map::new();
47
48        for (i, value) in row.columns.iter().enumerate() {
49            let key = self
50                .columns
51                .get(i)
52                .cloned()
53                .unwrap_or_else(|| format!("col{}", i));
54            let json_value = value_to_json(value);
55            obj.insert(key, json_value);
56        }
57
58        let json = serde_json::Value::Object(obj);
59        writeln!(writer, "{}", json)?;
60        Ok(())
61    }
62
63    fn write_footer(&mut self, _writer: &mut dyn Write) -> Result<()> {
64        // JSON Lines has no footer
65        Ok(())
66    }
67
68    fn supports_streaming(&self) -> bool {
69        true
70    }
71}
72
73/// Convert a Value to a serde_json::Value.
74fn value_to_json(value: &Value) -> serde_json::Value {
75    match value {
76        Value::Null => serde_json::Value::Null,
77        Value::Bool(b) => serde_json::Value::Bool(*b),
78        Value::Int(i) => serde_json::Value::Number((*i).into()),
79        Value::Float(f) => serde_json::Number::from_f64(*f)
80            .map(serde_json::Value::Number)
81            .unwrap_or(serde_json::Value::Null),
82        Value::Text(s) => serde_json::Value::String(s.clone()),
83        Value::Bytes(b) => {
84            // Encode bytes as array of numbers
85            serde_json::Value::Array(
86                b.iter()
87                    .map(|&byte| serde_json::Value::Number(byte.into()))
88                    .collect(),
89            )
90        }
91        Value::Vector(v) => serde_json::Value::Array(
92            v.iter()
93                .filter_map(|&f| serde_json::Number::from_f64(f as f64))
94                .map(serde_json::Value::Number)
95                .collect(),
96        ),
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::models::DataType;
104
105    fn test_columns() -> Vec<Column> {
106        vec![
107            Column::new("id", DataType::Int),
108            Column::new("name", DataType::Text),
109        ]
110    }
111
112    #[test]
113    fn test_jsonl_basic() {
114        let mut formatter = JsonlFormatter::new();
115        let mut output = Vec::new();
116
117        let columns = test_columns();
118        formatter.write_header(&mut output, &columns).unwrap();
119
120        let row = Row::new(vec![Value::Int(1), Value::Text("Alice".to_string())]);
121        formatter.write_row(&mut output, &row).unwrap();
122
123        formatter.write_footer(&mut output).unwrap();
124
125        let result = String::from_utf8(output).unwrap();
126        assert_eq!(result, r#"{"id":1,"name":"Alice"}"#.to_string() + "\n");
127    }
128
129    #[test]
130    fn test_jsonl_multiple_rows() {
131        let mut formatter = JsonlFormatter::new();
132        let mut output = Vec::new();
133
134        let columns = test_columns();
135        formatter.write_header(&mut output, &columns).unwrap();
136
137        let row1 = Row::new(vec![Value::Int(1), Value::Text("Alice".to_string())]);
138        let row2 = Row::new(vec![Value::Int(2), Value::Text("Bob".to_string())]);
139
140        formatter.write_row(&mut output, &row1).unwrap();
141        formatter.write_row(&mut output, &row2).unwrap();
142
143        formatter.write_footer(&mut output).unwrap();
144
145        let result = String::from_utf8(output).unwrap();
146        let lines: Vec<&str> = result.lines().collect();
147        assert_eq!(lines.len(), 2);
148        assert!(lines[0].contains("\"id\":1"));
149        assert!(lines[0].contains("\"name\":\"Alice\""));
150        assert!(lines[1].contains("\"id\":2"));
151        assert!(lines[1].contains("\"name\":\"Bob\""));
152    }
153
154    #[test]
155    fn test_jsonl_null_value() {
156        let mut formatter = JsonlFormatter::new();
157        let mut output = Vec::new();
158
159        let columns = test_columns();
160        formatter.write_header(&mut output, &columns).unwrap();
161
162        let row = Row::new(vec![Value::Int(1), Value::Null]);
163        formatter.write_row(&mut output, &row).unwrap();
164
165        let result = String::from_utf8(output).unwrap();
166        assert!(result.contains("\"name\":null"));
167    }
168
169    #[test]
170    fn test_jsonl_supports_streaming() {
171        let formatter = JsonlFormatter::new();
172        assert!(formatter.supports_streaming());
173    }
174}