1use std::path::{Path, PathBuf};
4
5use futures::stream;
6use rskit_errors::{AppError, AppResult, ErrorCode};
7use serde_json::Value;
8use tokio::sync::mpsc;
9
10use super::limits::{
11 MAX_CSV_RECORD_BYTES, MAX_JSON_ARRAY_BYTES, MAX_JSON_LINE_BYTES, validate_json_record,
12};
13use super::model::DatasetRecord;
14use super::ops::BoxRecordStream;
15
16pub trait DatasetReader: Send + Sync + 'static {
18 fn stream(self: Box<Self>) -> BoxRecordStream;
20}
21
22pub struct CsvReader {
24 path: PathBuf,
25}
26
27impl CsvReader {
28 #[must_use]
30 pub fn new(path: impl Into<PathBuf>) -> Self {
31 Self { path: path.into() }
32 }
33}
34
35impl DatasetReader for CsvReader {
36 fn stream(self: Box<Self>) -> BoxRecordStream {
37 let path = self.path;
38 stream_from_blocking_reader(move |tx| {
39 let file = match std::fs::File::open(&path) {
40 Ok(file) => file,
41 Err(error) => {
42 send_record(
43 &tx,
44 Err(AppError::new(
45 ErrorCode::Internal,
46 format!("failed to open CSV dataset {}: {error}", path.display()),
47 )),
48 );
49 return;
50 }
51 };
52 let mut reader = std::io::BufReader::new(file);
53
54 let headers = match read_line_bounded(&mut reader, MAX_CSV_RECORD_BYTES, "CSV header")
55 .and_then(|line| {
56 line.map_or_else(
57 || {
58 Err(AppError::new(
59 ErrorCode::InvalidInput,
60 format!("CSV dataset {} is missing headers", path.display()),
61 ))
62 },
63 |line| parse_csv_record(&line),
64 )
65 }) {
66 Ok(headers) => headers,
67 Err(error) => {
68 send_record(&tx, Err(error));
69 return;
70 }
71 };
72
73 loop {
74 match read_line_bounded(&mut reader, MAX_CSV_RECORD_BYTES, "CSV record")
75 .and_then(|line| line.map(|line| parse_csv_record(&line)).transpose())
76 {
77 Ok(Some(raw)) => {
78 let fields = headers
79 .iter()
80 .zip(raw.iter())
81 .map(|(key, value)| (key.to_string(), Value::String(value.to_string())))
82 .collect();
83 if !send_record(&tx, Ok(DatasetRecord::new(fields))) {
84 return;
85 }
86 }
87 Ok(None) => return,
88 Err(error) => {
89 send_record(&tx, Err(error));
90 return;
91 }
92 }
93 }
94 })
95 }
96}
97
98pub struct JsonLinesReader {
100 path: PathBuf,
101}
102
103impl JsonLinesReader {
104 #[must_use]
106 pub fn new(path: impl Into<PathBuf>) -> Self {
107 Self { path: path.into() }
108 }
109}
110
111impl DatasetReader for JsonLinesReader {
112 fn stream(self: Box<Self>) -> BoxRecordStream {
113 let path = self.path;
114 stream_from_blocking_reader(move |tx| {
115 let file = match std::fs::File::open(&path) {
116 Ok(file) => file,
117 Err(error) => {
118 send_record(
119 &tx,
120 Err(AppError::new(
121 ErrorCode::Internal,
122 format!(
123 "failed to open JSON Lines dataset {}: {error}",
124 path.display()
125 ),
126 )),
127 );
128 return;
129 }
130 };
131 let mut reader = std::io::BufReader::new(file);
132 loop {
133 let record = match read_json_line_bounded(&mut reader) {
134 Ok(Some(line)) => record_from_json_bytes(&line),
135 Ok(None) => return,
136 Err(error) => {
137 send_record(&tx, Err(error));
138 return;
139 }
140 };
141 if !send_record(&tx, record) {
142 return;
143 }
144 }
145 })
146 }
147}
148
149pub struct JsonArrayReader {
151 path: PathBuf,
152}
153
154impl JsonArrayReader {
155 #[must_use]
157 pub fn new(path: impl Into<PathBuf>) -> Self {
158 Self { path: path.into() }
159 }
160}
161
162impl DatasetReader for JsonArrayReader {
163 fn stream(self: Box<Self>) -> BoxRecordStream {
164 let path = self.path;
165 stream_from_blocking_reader(move |tx| {
166 let values =
167 match read_bounded_file(&path, MAX_JSON_ARRAY_BYTES as usize).and_then(|bytes| {
168 serde_json::from_slice::<Vec<Value>>(&bytes).map_err(|error| {
169 AppError::new(
170 ErrorCode::InvalidInput,
171 format!("failed to parse JSON dataset {}: {error}", path.display()),
172 )
173 })
174 }) {
175 Ok(values) => values,
176 Err(error) => {
177 send_record(&tx, Err(error));
178 return;
179 }
180 };
181
182 for value in values {
183 if !send_record(&tx, record_from_value(value)) {
184 return;
185 }
186 }
187 })
188 }
189}
190
191fn stream_from_blocking_reader(
192 producer: impl FnOnce(mpsc::Sender<AppResult<DatasetRecord>>) + Send + 'static,
193) -> BoxRecordStream {
194 if tokio::runtime::Handle::try_current().is_err() {
195 return Box::pin(stream::once(async {
196 Err(AppError::new(
197 ErrorCode::Internal,
198 "dataset readers require a Tokio runtime for blocking IO isolation",
199 ))
200 }));
201 }
202
203 let (tx, rx) = mpsc::channel(8);
204 let handle = tokio::task::spawn_blocking(move || producer(tx));
205 drop(handle);
206 Box::pin(stream::unfold(rx, |mut rx| async {
207 rx.recv().await.map(|item| (item, rx))
208 }))
209}
210
211fn send_record(
212 tx: &mpsc::Sender<AppResult<DatasetRecord>>,
213 item: AppResult<DatasetRecord>,
214) -> bool {
215 tx.blocking_send(item).is_ok()
216}
217
218fn record_from_json_bytes(line: &[u8]) -> AppResult<DatasetRecord> {
219 let value = serde_json::from_slice(line).map_err(|error| {
220 AppError::new(
221 ErrorCode::InvalidInput,
222 format!("failed to parse JSON record: {error}"),
223 )
224 })?;
225 record_from_value(value)
226}
227
228fn read_json_line_bounded(reader: &mut impl std::io::BufRead) -> AppResult<Option<Vec<u8>>> {
229 read_line_bounded(reader, MAX_JSON_LINE_BYTES, "JSON Lines record")
230}
231
232fn read_line_bounded(
233 reader: &mut impl std::io::BufRead,
234 max_bytes: usize,
235 label: &str,
236) -> AppResult<Option<Vec<u8>>> {
237 let mut line = Vec::new();
238
239 loop {
240 let available = reader.fill_buf().map_err(|error| {
241 AppError::new(
242 ErrorCode::Internal,
243 format!("failed to read JSON line: {error}"),
244 )
245 })?;
246 if available.is_empty() {
247 return if line.is_empty() {
248 Ok(None)
249 } else {
250 Ok(Some(line))
251 };
252 }
253
254 let consumed = match available.iter().position(|byte| *byte == b'\n') {
255 Some(pos) => {
256 let end = pos + 1;
257 append_line_chunk(&mut line, &available[..end], max_bytes, label)?;
258 end
259 }
260 None => {
261 append_line_chunk(&mut line, available, max_bytes, label)?;
262 available.len()
263 }
264 };
265 reader.consume(consumed);
266
267 if line.last() == Some(&b'\n') {
268 return Ok(Some(line));
269 }
270 }
271}
272
273fn append_line_chunk(
274 line: &mut Vec<u8>,
275 chunk: &[u8],
276 max_bytes: usize,
277 label: &str,
278) -> AppResult<()> {
279 if line.len().saturating_add(chunk.len()) > max_bytes {
280 return Err(AppError::new(
281 ErrorCode::InvalidInput,
282 format!("{label} exceeded max {max_bytes} bytes"),
283 ));
284 }
285 line.extend_from_slice(chunk);
286 Ok(())
287}
288
289fn parse_csv_record(line: &[u8]) -> AppResult<csv::StringRecord> {
290 let mut reader = csv::ReaderBuilder::new()
291 .has_headers(false)
292 .from_reader(line);
293 let mut records = reader.records();
294 match records.next() {
295 Some(Ok(record)) => Ok(record),
296 Some(Err(error)) => Err(AppError::new(
297 ErrorCode::InvalidInput,
298 format!("failed to parse CSV record: {error}"),
299 )),
300 None => Err(AppError::new(
301 ErrorCode::InvalidInput,
302 "CSV record was empty",
303 )),
304 }
305}
306
307fn read_bounded_file(path: &Path, max_bytes: usize) -> AppResult<Vec<u8>> {
308 use std::io::Read as _;
309
310 let mut file = std::fs::File::open(path).map_err(|error| {
311 AppError::new(
312 ErrorCode::Internal,
313 format!("failed to open JSON dataset {}: {error}", path.display()),
314 )
315 })?;
316 let mut bytes = Vec::new();
317 file.by_ref()
318 .take(max_bytes as u64 + 1)
319 .read_to_end(&mut bytes)
320 .map_err(|error| {
321 AppError::new(
322 ErrorCode::Internal,
323 format!("failed to read JSON dataset {}: {error}", path.display()),
324 )
325 })?;
326 if bytes.len() > max_bytes {
327 return Err(AppError::new(
328 ErrorCode::InvalidInput,
329 format!(
330 "JSON array dataset {} exceeded max {MAX_JSON_ARRAY_BYTES} bytes while reading",
331 path.display()
332 ),
333 ));
334 }
335 Ok(bytes)
336}
337
338fn record_from_value(value: Value) -> AppResult<DatasetRecord> {
339 validate_json_record(&value)?;
340 match value {
341 Value::Object(fields) => Ok(DatasetRecord::new(fields.into_iter().collect())),
342 _ => Err(AppError::new(
343 ErrorCode::InvalidInput,
344 "dataset record must be a JSON object",
345 )),
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use futures_util::StreamExt as _;
352 use serde_json::json;
353
354 use super::*;
355
356 #[test]
357 fn private_record_parsers_reject_invalid_shapes() {
358 assert!(record_from_json_bytes(b"not-json").is_err());
359 assert!(record_from_value(json!([1, 2])).is_err());
360 assert!(parse_csv_record(b"").is_err());
361 assert!(parse_csv_record(b"\xff").is_err());
362 }
363
364 #[test]
365 fn private_bounded_readers_report_io_errors() {
366 struct FailingRead;
367
368 impl std::io::Read for FailingRead {
369 fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
370 Err(std::io::Error::other("boom"))
371 }
372 }
373
374 impl std::io::BufRead for FailingRead {
375 fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
376 Err(std::io::Error::other("boom"))
377 }
378
379 fn consume(&mut self, _amt: usize) {}
380 }
381
382 let mut reader = FailingRead;
383 assert_eq!(
384 read_json_line_bounded(&mut reader).unwrap_err().code(),
385 ErrorCode::Internal
386 );
387
388 let dir = tempfile::tempdir().unwrap();
389 assert_eq!(
390 read_bounded_file(dir.path(), 16).unwrap_err().code(),
391 ErrorCode::Internal
392 );
393 }
394
395 #[test]
396 fn blocking_readers_report_missing_tokio_runtime() {
397 let reader = JsonLinesReader::new("unused.jsonl");
398 let mut stream = Box::new(reader).stream();
399 let err = futures::executor::block_on(stream.next())
400 .unwrap()
401 .unwrap_err();
402 assert_eq!(err.code(), ErrorCode::Internal);
403 assert!(err.to_string().contains("Tokio runtime"));
404 }
405
406 #[tokio::test]
407 async fn missing_record_readers_emit_errors_in_stream() {
408 let missing = std::env::temp_dir().join("rskit-dataset-missing-record-file");
409 let readers: Vec<Box<dyn DatasetReader>> = vec![
410 Box::new(CsvReader::new(&missing)),
411 Box::new(JsonLinesReader::new(&missing)),
412 Box::new(JsonArrayReader::new(&missing)),
413 ];
414
415 for reader in readers {
416 let mut stream = reader.stream();
417 assert!(stream.next().await.unwrap().is_err());
418 }
419 }
420}