apif_source_row/
ndjson.rs1use 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 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}