1use super::{Parser, ParserConfig, RdfFormat};
25use crate::model::Quad;
26use crate::{OxirsError, Result};
27use std::future::Future;
28use std::pin::Pin;
29
30#[cfg(feature = "async")]
33const DEFAULT_MAX_BUFFER_SIZE: usize = 256 * 1024 * 1024;
34
35#[cfg(feature = "async")]
40pub struct AsyncStreamingParser {
41 format: RdfFormat,
42 config: ParserConfig,
43 progress_callback: Option<Box<dyn Fn(usize) + Send + Sync>>,
44 chunk_size: usize,
45 max_buffer_size: usize,
46}
47
48#[cfg(feature = "async")]
49impl AsyncStreamingParser {
50 pub fn new(format: RdfFormat) -> Self {
52 AsyncStreamingParser {
53 format,
54 config: ParserConfig::default(),
55 progress_callback: None,
56 chunk_size: 8192, max_buffer_size: DEFAULT_MAX_BUFFER_SIZE,
58 }
59 }
60
61 pub fn with_progress_callback<F>(mut self, callback: F) -> Self
63 where
64 F: Fn(usize) + Send + Sync + 'static,
65 {
66 self.progress_callback = Some(Box::new(callback));
67 self
68 }
69
70 pub fn with_chunk_size(mut self, chunk_size: usize) -> Self {
72 self.chunk_size = chunk_size;
73 self
74 }
75
76 pub fn with_max_buffer_size(mut self, max_buffer_size: usize) -> Self {
82 self.max_buffer_size = max_buffer_size;
83 self
84 }
85
86 pub fn with_error_tolerance(mut self, ignore_errors: bool) -> Self {
88 self.config.ignore_errors = ignore_errors;
89 self
90 }
91
92 pub async fn parse_stream<R, F, Fut>(&self, mut reader: R, mut handler: F) -> Result<()>
94 where
95 R: tokio::io::AsyncRead + Unpin,
96 F: FnMut(Quad) -> Fut,
97 Fut: Future<Output = Result<()>>,
98 {
99 use tokio::io::AsyncReadExt;
100
101 let mut buffer = Vec::with_capacity(self.chunk_size);
102 let mut accumulated_data = String::new();
103 let mut bytes_processed = 0usize;
104 let mut line_buffer = String::new();
105
106 loop {
107 buffer.clear();
108 buffer.resize(self.chunk_size, 0);
109
110 let bytes_read = reader.read(&mut buffer).await?;
111
112 if bytes_read == 0 {
113 break; }
115
116 buffer.truncate(bytes_read);
117 bytes_processed += bytes_read;
118
119 let chunk_str = String::from_utf8_lossy(&buffer);
121 accumulated_data.push_str(&chunk_str);
122
123 if matches!(self.format, RdfFormat::NTriples | RdfFormat::NQuads) {
126 self.process_lines_async(&mut accumulated_data, &mut line_buffer, &mut handler)
127 .await?;
128 } else if accumulated_data.len() > self.max_buffer_size {
129 return Err(OxirsError::Parse(format!(
133 "AsyncStreamingParser: {:?} is not incrementally streamed and the \
134 source exceeded the configured max_buffer_size of {} bytes \
135 (buffered {} bytes so far). Increase the limit via \
136 with_max_buffer_size(), split the document, or use \
137 RdfFormat::NTriples/NQuads for true streaming.",
138 self.format,
139 self.max_buffer_size,
140 accumulated_data.len()
141 )));
142 }
143
144 if let Some(ref callback) = self.progress_callback {
146 callback(bytes_processed);
147 }
148 }
149
150 if !accumulated_data.is_empty() {
152 match self.format {
153 RdfFormat::NTriples | RdfFormat::NQuads => {
154 accumulated_data.push_str(&line_buffer);
156 self.process_lines_async(
157 &mut accumulated_data,
158 &mut String::new(),
159 &mut handler,
160 )
161 .await?;
162 }
163 _ => {
164 let parser = Parser::with_config(self.format, self.config.clone());
171 let quads = parser.parse_str_to_quads(&accumulated_data)?;
172 for quad in quads {
173 handler(quad).await?;
174 }
175 }
176 }
177 }
178
179 Ok(())
180 }
181
182 async fn process_lines_async<F, Fut>(
184 &self,
185 accumulated_data: &mut String,
186 line_buffer: &mut String,
187 handler: &mut F,
188 ) -> Result<()>
189 where
190 F: FnMut(Quad) -> Fut,
191 Fut: Future<Output = Result<()>>,
192 {
193 let mut full_data = line_buffer.clone();
195 full_data.push_str(accumulated_data);
196
197 let mut last_newline_pos = 0;
198
199 for (pos, _) in full_data.match_indices('\n') {
201 let line = &full_data[last_newline_pos..pos];
202 last_newline_pos = pos + 1;
203
204 if let Some(quad) = self.parse_line(line)? {
206 handler(quad).await?;
207 }
208 }
209
210 line_buffer.clear();
212 if last_newline_pos < full_data.len() {
213 line_buffer.push_str(&full_data[last_newline_pos..]);
214 }
215
216 accumulated_data.clear();
217 Ok(())
218 }
219
220 fn parse_line(&self, line: &str) -> Result<Option<Quad>> {
222 let parser = Parser::with_config(self.format, self.config.clone());
223
224 match self.format {
225 RdfFormat::NTriples => parser.parse_ntriples_line(line),
226 RdfFormat::NQuads => {
227 parser.parse_ntriples_line(line)
230 }
231 _ => Err(OxirsError::Parse(
232 "Unsupported format for line parsing".to_string(),
233 )),
234 }
235 }
236
237 pub async fn parse_bytes<F, Fut>(&self, data: &[u8], handler: F) -> Result<()>
239 where
240 F: FnMut(Quad) -> Fut,
241 Fut: Future<Output = Result<()>>,
242 {
243 use std::io::Cursor;
244 let cursor = Cursor::new(data);
245 self.parse_stream(cursor, handler).await
246 }
247
248 pub async fn parse_str_async<F, Fut>(&self, data: &str, handler: F) -> Result<()>
250 where
251 F: FnMut(Quad) -> Fut,
252 Fut: Future<Output = Result<()>>,
253 {
254 let bytes = data.as_bytes();
255 self.parse_bytes(bytes, handler).await
256 }
257
258 pub async fn parse_str_to_quads_async(&self, data: &str) -> Result<Vec<Quad>> {
260 use std::sync::Arc;
261 use tokio::sync::Mutex;
262
263 let quads = Arc::new(Mutex::new(Vec::new()));
264 let quads_clone = Arc::clone(&quads);
265
266 self.parse_str_async(data, move |quad| {
267 let quads = Arc::clone(&quads_clone);
268 async move {
269 quads.lock().await.push(quad);
270 Ok(())
271 }
272 })
273 .await?;
274
275 let result = quads.lock().await;
276 Ok(result.clone())
277 }
278}
279
280#[cfg(feature = "async")]
282#[derive(Debug, Clone)]
283pub struct ParseProgress {
284 pub bytes_processed: usize,
285 pub quads_parsed: usize,
286 pub errors_encountered: usize,
287 pub estimated_total_bytes: Option<usize>,
288}
289
290#[cfg(feature = "async")]
291impl ParseProgress {
292 pub fn completion_percentage(&self) -> Option<f64> {
294 self.estimated_total_bytes.map(|total| {
295 if total == 0 {
296 100.0
297 } else {
298 (self.bytes_processed as f64 / total as f64) * 100.0
299 }
300 })
301 }
302}
303
304#[cfg(feature = "async")]
306pub trait AsyncRdfSink: Send + Sync {
307 fn process_quad(&mut self, quad: Quad)
309 -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
310
311 fn finalize(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>>;
313}
314
315#[cfg(feature = "async")]
317pub struct MemoryAsyncSink {
318 quads: Vec<Quad>,
319}
320
321#[cfg(feature = "async")]
322impl MemoryAsyncSink {
323 pub fn new() -> Self {
324 MemoryAsyncSink { quads: Vec::new() }
325 }
326
327 pub fn into_quads(self) -> Vec<Quad> {
328 self.quads
329 }
330}
331
332#[cfg(feature = "async")]
333impl Default for MemoryAsyncSink {
334 fn default() -> Self {
335 Self::new()
336 }
337}
338
339#[cfg(feature = "async")]
340impl AsyncRdfSink for MemoryAsyncSink {
341 fn process_quad(
342 &mut self,
343 quad: Quad,
344 ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
345 Box::pin(async move {
346 self.quads.push(quad);
347 Ok(())
348 })
349 }
350
351 fn finalize(&mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
352 Box::pin(async move { Ok(()) })
353 }
354}
355
356#[cfg(all(test, feature = "async"))]
357mod tests {
358 use super::*;
359 use std::io::Cursor;
360 use std::sync::{Arc, Mutex};
361
362 #[tokio::test]
366 async fn test_ntriples_streams_correctly() {
367 let data = "<http://example.org/s1> <http://example.org/p> \"o1\" .\n\
368 <http://example.org/s2> <http://example.org/p> \"o2\" .\n";
369 let reader = Cursor::new(data.as_bytes());
370 let parser = AsyncStreamingParser::new(RdfFormat::NTriples);
371
372 let quads = Arc::new(Mutex::new(Vec::new()));
373 let quads_clone = Arc::clone(&quads);
374 parser
375 .parse_stream(reader, move |quad| {
376 let quads = Arc::clone(&quads_clone);
377 async move {
378 quads
379 .lock()
380 .map_err(|_| OxirsError::Parse("poisoned".into()))?
381 .push(quad);
382 Ok(())
383 }
384 })
385 .await
386 .expect("N-Triples streaming should succeed");
387
388 assert_eq!(quads.lock().expect("lock").len(), 2);
389 }
390
391 #[tokio::test]
399 async fn test_turtle_buffered_parsing_produces_correct_quads() {
400 let data = r#"@prefix ex: <http://example.org/> .
401ex:alice ex:knows ex:bob ."#;
402 let reader = Cursor::new(data.as_bytes());
403 let parser = AsyncStreamingParser::new(RdfFormat::Turtle);
404
405 let quads = Arc::new(Mutex::new(Vec::new()));
406 let quads_clone = Arc::clone(&quads);
407 parser
408 .parse_stream(reader, move |quad| {
409 let quads = Arc::clone(&quads_clone);
410 async move {
411 quads
412 .lock()
413 .map_err(|_| OxirsError::Parse("poisoned".into()))?
414 .push(quad);
415 Ok(())
416 }
417 })
418 .await
419 .expect("buffered Turtle parsing should succeed");
420
421 assert_eq!(quads.lock().expect("lock").len(), 1);
422 }
423
424 #[tokio::test]
429 async fn test_non_streaming_format_exceeding_max_buffer_errors_loudly() {
430 let data = r#"@prefix ex: <http://example.org/> .
431ex:alice ex:knows ex:bob, ex:carol, ex:dave, ex:eve, ex:frank ."#;
432 let reader = Cursor::new(data.as_bytes());
433 let parser = AsyncStreamingParser::new(RdfFormat::Turtle)
435 .with_chunk_size(16)
436 .with_max_buffer_size(8);
437
438 let result = parser.parse_stream(reader, |_quad| async { Ok(()) }).await;
439
440 assert!(
441 result.is_err(),
442 "exceeding max_buffer_size must be a loud error, not silent unbounded buffering"
443 );
444 let message = result.unwrap_err().to_string();
445 assert!(
446 message.contains("max_buffer_size") || message.contains("not incrementally streamed"),
447 "error should clearly explain the buffering limitation: {message}"
448 );
449 }
450
451 #[tokio::test]
456 async fn test_ntriples_ignores_max_buffer_size_cap() {
457 let mut data = String::new();
458 for i in 0..50 {
459 data.push_str(&format!(
460 "<http://example.org/s{i}> <http://example.org/p> \"o{i}\" .\n"
461 ));
462 }
463 let reader = Cursor::new(data.as_bytes());
464 let parser = AsyncStreamingParser::new(RdfFormat::NTriples)
465 .with_chunk_size(16)
466 .with_max_buffer_size(8);
467
468 let quads = Arc::new(Mutex::new(Vec::new()));
469 let quads_clone = Arc::clone(&quads);
470 parser
471 .parse_stream(reader, move |quad| {
472 let quads = Arc::clone(&quads_clone);
473 async move {
474 quads
475 .lock()
476 .map_err(|_| OxirsError::Parse("poisoned".into()))?
477 .push(quad);
478 Ok(())
479 }
480 })
481 .await
482 .expect("true streaming formats must ignore the buffered-format cap");
483
484 assert_eq!(quads.lock().expect("lock").len(), 50);
485 }
486}