Skip to main content

lora_io/
jsonl.rs

1//! JSON Lines (NDJSON) codec. One JSON object per line.
2
3use std::io::{BufRead, Write};
4
5use lora_executor::{LoraValue, Row};
6use serde_json::Value as J;
7
8use super::format::{
9    invalid_data, row_parse_io_error, RowDecoder, RowEncoder, RowParseError, StreamingRowDecoder,
10};
11use super::value_json::{lora_value_from_json, lora_value_to_json};
12
13pub struct JsonlEncoder<W: Write> {
14    writer: W,
15}
16
17impl<W: Write> JsonlEncoder<W> {
18    pub fn new(writer: W) -> Self {
19        Self { writer }
20    }
21
22    pub fn into_inner(self) -> W {
23        self.writer
24    }
25}
26
27impl<W: Write> RowEncoder for JsonlEncoder<W> {
28    fn begin(&mut self, _columns: &[String]) -> std::io::Result<()> {
29        Ok(())
30    }
31
32    fn write_row(&mut self, row: &Row) -> std::io::Result<()> {
33        let mut obj = serde_json::Map::with_capacity(row.len());
34        for (_, name, value) in row.iter_named() {
35            obj.insert(name.into_owned(), lora_value_to_json(value));
36        }
37        serde_json::to_writer(&mut self.writer, &J::Object(obj))?;
38        self.writer.write_all(b"\n")
39    }
40
41    fn write_named_row(&mut self, columns: &[(String, LoraValue)]) -> std::io::Result<()> {
42        let mut obj = serde_json::Map::with_capacity(columns.len());
43        for (name, value) in columns {
44            obj.insert(name.clone(), lora_value_to_json(value));
45        }
46        serde_json::to_writer(&mut self.writer, &J::Object(obj))?;
47        self.writer.write_all(b"\n")
48    }
49
50    fn finish(&mut self) -> std::io::Result<()> {
51        self.writer.flush()
52    }
53}
54
55pub struct JsonlDecoder<R: BufRead> {
56    reader: R,
57    buf: String,
58}
59
60impl<R: BufRead> JsonlDecoder<R> {
61    pub fn new(reader: R) -> Self {
62        Self {
63            reader,
64            buf: String::new(),
65        }
66    }
67}
68
69impl<R: BufRead> RowDecoder for JsonlDecoder<R> {
70    fn header(&mut self) -> std::io::Result<Option<Vec<String>>> {
71        Ok(None)
72    }
73
74    fn next_row(&mut self) -> std::io::Result<Option<Vec<(String, LoraValue)>>> {
75        loop {
76            self.buf.clear();
77            let read = self.reader.read_line(&mut self.buf)?;
78            if read == 0 {
79                return Ok(None);
80            }
81            let line = self.buf.trim_matches(|c: char| c == '\r' || c == '\n');
82            if line.is_empty() {
83                continue;
84            }
85            let v: J = serde_json::from_str(line).map_err(invalid_data)?;
86            let J::Object(obj) = v else {
87                return Err(invalid_data(format!(
88                    "expected JSON object per line, found {}",
89                    type_name(&serde_json::from_str(line).unwrap_or(J::Null))
90                )));
91            };
92            let mut out = Vec::with_capacity(obj.len());
93            for (k, raw) in obj {
94                out.push((k, lora_value_from_json(raw).map_err(invalid_data)?));
95            }
96            return Ok(Some(out));
97        }
98    }
99}
100
101fn type_name(v: &J) -> &'static str {
102    match v {
103        J::Null => "null",
104        J::Bool(_) => "bool",
105        J::Number(_) => "number",
106        J::String(_) => "string",
107        J::Array(_) => "array",
108        J::Object(_) => "object",
109    }
110}
111
112/// Push-based JSONL decoder. Accumulates bytes between calls to
113/// [`Self::feed`], splits on `\n`, parses each completed line as a
114/// JSON object, and queues the resulting record for [`Self::drain`].
115pub struct StreamingJsonlDecoder {
116    /// Bytes received but not yet terminated by a newline. Cleared
117    /// every time a `\n` is observed (the line up to and including
118    /// it is consumed; the residual tail stays here).
119    buffer: Vec<u8>,
120    /// Completed records waiting to be drained.
121    completed: Vec<Vec<(String, LoraValue)>>,
122    bytes_fed: u64,
123    rows_emitted: u64,
124    /// 1-indexed counter of non-blank lines seen so far. Used to
125    /// attribute parse errors to a specific record. Advances even
126    /// when a record fails (or is skipped in permissive mode).
127    record_index: u64,
128    permissive: bool,
129    errors: Vec<RowParseError>,
130}
131
132impl Default for StreamingJsonlDecoder {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138impl StreamingJsonlDecoder {
139    pub fn new() -> Self {
140        Self {
141            buffer: Vec::with_capacity(64 * 1024),
142            completed: Vec::new(),
143            bytes_fed: 0,
144            rows_emitted: 0,
145            record_index: 0,
146            permissive: false,
147            errors: Vec::new(),
148        }
149    }
150
151    fn parse_line(&mut self, line: &[u8]) -> std::io::Result<()> {
152        let s = std::str::from_utf8(line).map_err(invalid_data)?;
153        let trimmed = s.trim_matches(|c: char| c == '\r' || c == '\n' || c == ' ' || c == '\t');
154        if trimmed.is_empty() {
155            return Ok(());
156        }
157        self.record_index += 1;
158        match parse_jsonl_object(trimmed) {
159            Ok(record) => {
160                self.completed.push(record);
161                self.rows_emitted += 1;
162                Ok(())
163            }
164            Err(message) => self.report_error(message, trimmed),
165        }
166    }
167
168    fn report_error(&mut self, message: String, raw: &str) -> std::io::Result<()> {
169        let err = RowParseError {
170            row: self.record_index,
171            column: None,
172            raw_sample: RowParseError::make_sample(raw),
173            message,
174        };
175        if self.permissive {
176            self.errors.push(err);
177            Ok(())
178        } else {
179            Err(row_parse_io_error(err))
180        }
181    }
182}
183
184fn parse_jsonl_object(line: &str) -> Result<Vec<(String, LoraValue)>, String> {
185    let v: J = serde_json::from_str(line).map_err(|e| e.to_string())?;
186    let J::Object(obj) = v else {
187        return Err(format!(
188            "expected JSON object per line, found {}",
189            type_name(&v)
190        ));
191    };
192    let mut record = Vec::with_capacity(obj.len());
193    for (k, raw) in obj {
194        let value = lora_value_from_json(raw).map_err(|e| format!("key `{k}`: {e}"))?;
195        record.push((k, value));
196    }
197    Ok(record)
198}
199
200impl StreamingRowDecoder for StreamingJsonlDecoder {
201    fn feed(&mut self, chunk: &[u8]) -> std::io::Result<()> {
202        if chunk.is_empty() {
203            return Ok(());
204        }
205        self.bytes_fed += chunk.len() as u64;
206        self.buffer.extend_from_slice(chunk);
207
208        // Walk the buffer extracting newline-terminated lines.
209        // Drain the prefix containing complete lines and re-buffer
210        // any partial tail. We collect line spans first so we can
211        // borrow `self.buffer` immutably while iterating and only
212        // call the (mutating) parser afterwards.
213        let mut lines: Vec<Vec<u8>> = Vec::new();
214        let mut last_end = 0usize;
215        for (idx, &b) in self.buffer.iter().enumerate() {
216            if b == b'\n' {
217                lines.push(self.buffer[last_end..=idx].to_vec());
218                last_end = idx + 1;
219            }
220        }
221        if last_end > 0 {
222            self.buffer.drain(..last_end);
223        }
224        for line in lines {
225            self.parse_line(&line)?;
226        }
227        Ok(())
228    }
229
230    fn drain(&mut self) -> std::io::Result<Vec<Vec<(String, LoraValue)>>> {
231        Ok(std::mem::take(&mut self.completed))
232    }
233
234    fn finish(&mut self) -> std::io::Result<Vec<Vec<(String, LoraValue)>>> {
235        // Parse the residual buffer as one final line if it has any
236        // non-whitespace content (handles files without a trailing
237        // newline).
238        let leftover = std::mem::take(&mut self.buffer);
239        if !leftover.is_empty() {
240            self.parse_line(&leftover)?;
241        }
242        Ok(std::mem::take(&mut self.completed))
243    }
244
245    fn header(&self) -> Option<&[String]> {
246        None
247    }
248
249    fn bytes_fed(&self) -> u64 {
250        self.bytes_fed
251    }
252
253    fn rows_emitted(&self) -> u64 {
254        self.rows_emitted
255    }
256
257    fn set_permissive(&mut self, on: bool) {
258        self.permissive = on;
259    }
260
261    fn take_errors(&mut self) -> Vec<RowParseError> {
262        std::mem::take(&mut self.errors)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use std::io::Cursor;
270
271    #[test]
272    fn round_trip_scalars() {
273        let mut buf = Vec::new();
274        {
275            let mut enc = JsonlEncoder::new(&mut buf);
276            enc.begin(&["name".into(), "age".into()]).unwrap();
277            enc.write_named_row(&[
278                ("name".into(), LoraValue::String("alice".into())),
279                ("age".into(), LoraValue::Int(30)),
280            ])
281            .unwrap();
282            enc.write_named_row(&[
283                ("name".into(), LoraValue::String("bob".into())),
284                ("age".into(), LoraValue::Int(25)),
285            ])
286            .unwrap();
287            enc.finish().unwrap();
288        }
289        let text = std::str::from_utf8(&buf).unwrap();
290        let lines: Vec<_> = text.lines().collect();
291        assert_eq!(lines.len(), 2);
292        assert!(lines[0].contains("\"alice\""));
293
294        // serde_json::Map (without preserve_order) sorts keys, so we
295        // assert by lookup rather than position.
296        let mut dec = JsonlDecoder::new(Cursor::new(buf));
297        let r1: std::collections::BTreeMap<_, _> =
298            dec.next_row().unwrap().unwrap().into_iter().collect();
299        assert_eq!(r1.get("name"), Some(&LoraValue::String("alice".into())));
300        assert_eq!(r1.get("age"), Some(&LoraValue::Int(30)));
301        let r2: std::collections::BTreeMap<_, _> =
302            dec.next_row().unwrap().unwrap().into_iter().collect();
303        assert_eq!(r2.get("name"), Some(&LoraValue::String("bob".into())));
304        assert_eq!(r2.get("age"), Some(&LoraValue::Int(25)));
305        assert!(dec.next_row().unwrap().is_none());
306    }
307
308    #[test]
309    fn streaming_split_across_chunks() {
310        // Half a record, then the rest, then a second record split
311        // across two chunks, then a chunk with no newline (carries
312        // into finish()).
313        let mut dec = StreamingJsonlDecoder::new();
314        dec.feed(b"{\"a\":").unwrap();
315        assert_eq!(dec.drain().unwrap().len(), 0);
316        dec.feed(b"1}\n{\"b\":").unwrap();
317        let rows1 = dec.drain().unwrap();
318        assert_eq!(rows1.len(), 1);
319        assert_eq!(rows1[0][0], ("a".into(), LoraValue::Int(1)));
320        dec.feed(b"2}\n{\"c\":3}").unwrap();
321        let rows2 = dec.drain().unwrap();
322        assert_eq!(rows2.len(), 1);
323        assert_eq!(rows2[0][0], ("b".into(), LoraValue::Int(2)));
324
325        // Final unterminated line drained by finish().
326        let rows3 = dec.finish().unwrap();
327        assert_eq!(rows3.len(), 1);
328        assert_eq!(rows3[0][0], ("c".into(), LoraValue::Int(3)));
329        assert_eq!(dec.rows_emitted(), 3);
330    }
331
332    #[test]
333    fn streaming_skips_blank_lines() {
334        let mut dec = StreamingJsonlDecoder::new();
335        dec.feed(b"\n\n{\"a\":1}\n\n").unwrap();
336        let rows = dec.drain().unwrap();
337        assert_eq!(rows.len(), 1);
338        assert!(dec.finish().unwrap().is_empty());
339    }
340
341    #[test]
342    fn streaming_strict_mode_bubbles_row_context() {
343        let mut dec = StreamingJsonlDecoder::new();
344        let err = dec.feed(b"{\"a\":1}\nnot json\n").unwrap_err();
345        let parse = super::super::format::downcast_row_parse_error(&err)
346            .expect("error should carry RowParseError");
347        assert_eq!(parse.row, 2);
348        assert!(parse.column.is_none());
349        assert!(parse.raw_sample.contains("not json"));
350    }
351
352    #[test]
353    fn streaming_permissive_mode_skips_and_collects() {
354        let mut dec = StreamingJsonlDecoder::new();
355        dec.set_permissive(true);
356        dec.feed(b"{\"a\":1}\nnot json\n{\"b\":2}\n").unwrap();
357        let rows = dec.drain().unwrap();
358        assert_eq!(rows.len(), 2);
359        assert_eq!(rows[0][0], ("a".into(), LoraValue::Int(1)));
360        assert_eq!(rows[1][0], ("b".into(), LoraValue::Int(2)));
361        let errors = dec.take_errors();
362        assert_eq!(errors.len(), 1);
363        assert_eq!(errors[0].row, 2);
364        assert_eq!(dec.rows_emitted(), 2);
365        // Errors are drained — second call is empty.
366        assert!(dec.take_errors().is_empty());
367    }
368
369    #[test]
370    fn blank_lines_skipped() {
371        let input = "\n{\"a\":1}\n\n{\"b\":2}\n";
372        let mut dec = JsonlDecoder::new(Cursor::new(input));
373        let r1 = dec.next_row().unwrap().unwrap();
374        assert_eq!(r1[0], ("a".into(), LoraValue::Int(1)));
375        let r2 = dec.next_row().unwrap().unwrap();
376        assert_eq!(r2[0], ("b".into(), LoraValue::Int(2)));
377        assert!(dec.next_row().unwrap().is_none());
378    }
379}