Skip to main content

apif_source_row/
ndjson.rs

1use crate::SourceReader;
2use crate::SourceRow;
3use anyhow::Result;
4use apif_source_error::SourceError;
5use std::io::{BufRead, BufReader, Read, Seek};
6
7pub struct NdjsonReader<R> {
8    reader: BufReader<R>,
9    headers: Vec<String>,
10    /// First row is buffered during header discovery
11    pending_first: Option<SourceRow>,
12    row_number: usize,
13    finished: bool,
14}
15
16fn json_value_to_string(v: Option<&serde_json::Value>) -> String {
17    match v {
18        None => String::new(),
19        Some(serde_json::Value::Null) => String::new(),
20        Some(serde_json::Value::String(s)) => s.clone(),
21        Some(serde_json::Value::Number(n)) => n.to_string(),
22        Some(serde_json::Value::Bool(b)) => b.to_string(),
23        Some(other) => other.to_string(),
24    }
25}
26
27impl<R: Read> NdjsonReader<R> {
28    pub fn new(reader: BufReader<R>) -> Self {
29        Self {
30            reader,
31            headers: Vec::new(),
32            pending_first: None,
33            row_number: 0,
34            finished: false,
35        }
36    }
37
38    fn read_next_line(&mut self) -> Result<Option<String>> {
39        loop {
40            let mut line = String::new();
41            if self.reader.read_line(&mut line)? == 0 {
42                return Ok(None);
43            }
44            let trimmed = line.trim_ascii();
45            if trimmed.is_empty() || trimmed.starts_with('#') {
46                continue;
47            }
48            return Ok(Some(trimmed.to_string()));
49        }
50    }
51
52    fn parse_line(&self, line: &str, line_num: usize) -> Result<Vec<String>> {
53        let value: serde_json::Value = serde_json::from_str(line)
54            .map_err(|e| SourceError::InvalidJson(line_num, e.to_string()))?;
55
56        let obj = match &value {
57            serde_json::Value::Object(m) => m,
58            _ => {
59                return Err(
60                    SourceError::InvalidJson(line_num, "expected JSON object".into()).into(),
61                );
62            }
63        };
64
65        let values: Vec<String> = self
66            .headers
67            .iter()
68            .map(|k| json_value_to_string(obj.get(k)))
69            .collect();
70        Ok(values)
71    }
72
73    fn discover_headers(&mut self) -> Result<()> {
74        if !self.headers.is_empty() {
75            return Ok(());
76        }
77
78        let Some(line) = self.read_next_line()? else {
79            return Ok(());
80        };
81
82        let value: serde_json::Value =
83            serde_json::from_str(&line).map_err(|e| SourceError::InvalidJson(1, e.to_string()))?;
84
85        let obj = match &value {
86            serde_json::Value::Object(m) => m,
87            _ => return Err(SourceError::InvalidJson(1, "expected JSON object".into()).into()),
88        };
89
90        let mut keys: Vec<String> = obj.keys().cloned().collect();
91        keys.sort();
92        self.headers = keys;
93        self.row_number = 1;
94
95        let values: Vec<String> = self
96            .headers
97            .iter()
98            .map(|k| json_value_to_string(obj.get(k)))
99            .collect();
100        self.pending_first = Some(SourceRow::new(&self.headers, values));
101        Ok(())
102    }
103}
104
105impl<R: Read + Send> SourceReader for NdjsonReader<R> {
106    fn next_row(&mut self) -> Result<Option<SourceRow>> {
107        if self.finished {
108            return Ok(None);
109        }
110
111        if let Some(row) = self.pending_first.take() {
112            return Ok(Some(row));
113        }
114
115        if self.headers.is_empty() {
116            self.discover_headers()?;
117            if let Some(row) = self.pending_first.take() {
118                return Ok(Some(row));
119            }
120        }
121
122        let Some(line) = self.read_next_line()? else {
123            self.finished = true;
124            return Ok(None);
125        };
126
127        self.row_number += 1;
128        let values = self.parse_line(&line, self.row_number)?;
129        Ok(Some(SourceRow::new(&self.headers, values)))
130    }
131
132    fn headers(&self) -> &[String] {
133        &self.headers
134    }
135
136    fn supports_reset(&self) -> bool {
137        true
138    }
139
140    fn reset(&mut self) -> Result<()> {
141        Ok(())
142    }
143}
144
145impl<R: Read + Seek + Send> NdjsonReader<R> {
146    pub fn reset_seekable(&mut self) -> Result<()> {
147        self.reader.seek(std::io::SeekFrom::Start(0))?;
148        self.headers.clear();
149        self.pending_first = None;
150        self.row_number = 0;
151        self.finished = false;
152        Ok(())
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use std::io::Cursor;
160
161    fn cursor(data: &str) -> BufReader<Cursor<&str>> {
162        BufReader::new(Cursor::new(data))
163    }
164
165    #[test]
166    fn ndjson_reads_basic_rows() {
167        let data = "{\"id\":1,\"name\":\"Alice\"}\n{\"id\":2,\"name\":\"Bob\"}\n";
168        let mut reader = NdjsonReader::new(cursor(data));
169
170        let row1 = reader.next_row().unwrap().unwrap();
171        assert_eq!(row1.get("id"), Some("1"));
172        assert_eq!(row1.get("name"), Some("Alice"));
173
174        let row2 = reader.next_row().unwrap().unwrap();
175        assert_eq!(row2.get("name"), Some("Bob"));
176
177        assert!(reader.next_row().unwrap().is_none());
178    }
179
180    #[test]
181    fn ndjson_discover_headers_sorted() {
182        let data = "{\"z\":1,\"a\":2}\n";
183        let mut reader = NdjsonReader::new(cursor(data));
184        let _row = reader.next_row().unwrap().unwrap();
185        assert_eq!(reader.headers(), &["a", "z"]);
186    }
187
188    #[test]
189    fn ndjson_skips_comments_and_blank_lines() {
190        let data = "# comment\n\n{\"id\":1,\"val\":\"hello\"}\n\n{\"id\":2,\"val\":\"world\"}\n";
191        let mut reader = NdjsonReader::new(cursor(data));
192        let row1 = reader.next_row().unwrap().unwrap();
193        assert_eq!(row1.get("val"), Some("hello"));
194        let row2 = reader.next_row().unwrap().unwrap();
195        assert_eq!(row2.get("val"), Some("world"));
196        assert!(reader.next_row().unwrap().is_none());
197    }
198
199    #[test]
200    fn ndjson_empty_returns_none() {
201        let data = "";
202        let mut reader = NdjsonReader::new(cursor(data));
203        assert!(reader.next_row().unwrap().is_none());
204    }
205
206    #[test]
207    fn ndjson_handles_null_and_bool() {
208        let data = "{\"s\":null,\"b\":true,\"n\":42}\n";
209        let mut reader = NdjsonReader::new(cursor(data));
210        let row = reader.next_row().unwrap().unwrap();
211        assert_eq!(row.get("s"), Some(""));
212        assert_eq!(row.get("b"), Some("true"));
213        assert_eq!(row.get("n"), Some("42"));
214    }
215
216    #[test]
217    fn ndjson_invalid_json_errors() {
218        let data = "{\"id\":1}\nnot json\n";
219        let mut reader = NdjsonReader::new(cursor(data));
220        let _row1 = reader.next_row().unwrap().unwrap();
221        let result = reader.next_row();
222        assert!(result.is_err());
223    }
224
225    #[test]
226    fn ndjson_missing_key_defaults_empty() {
227        let data = "{\"id\":1,\"name\":\"Alice\"}\n{\"id\":2}\n";
228        let mut reader = NdjsonReader::new(cursor(data));
229        let _row1 = reader.next_row().unwrap();
230        let row2 = reader.next_row().unwrap().unwrap();
231        assert_eq!(row2.get("name"), Some(""));
232        assert_eq!(row2.get("id"), Some("2"));
233    }
234}