Skip to main content

oxttl/
nquads.rs

1//! A [N-Quads](https://www.w3.org/TR/n-quads/) streaming parser implemented by [`NQuadsParser`]
2//! and a serializer implemented by [`NQuadsSerializer`].
3
4use crate::MIN_PARALLEL_CHUNK_SIZE;
5use crate::chunker::{get_ntriples_file_chunks, get_ntriples_slice_chunks};
6use crate::line_formats::NQuadsRecognizer;
7#[cfg(feature = "async-tokio")]
8use crate::toolkit::TokioAsyncReaderIterator;
9use crate::toolkit::{Parser, ReaderIterator, SliceIterator, TurtleParseError, TurtleSyntaxError};
10use oxrdf::{Quad, QuadRef};
11use std::fs::File;
12use std::io::{self, Read, Seek, SeekFrom, Take, Write};
13use std::path::Path;
14#[cfg(feature = "async-tokio")]
15use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
16
17/// A [N-Quads](https://www.w3.org/TR/n-quads/) streaming parser.
18///
19/// Count the number of people:
20/// ```
21/// use oxrdf::{NamedNodeRef, vocab::rdf};
22/// use oxttl::NQuadsParser;
23///
24/// let file = r#"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
25/// <http://example.com/foo> <http://schema.org/name> "Foo" .
26/// <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
27/// <http://example.com/bar> <http://schema.org/name> "Bar" ."#;
28///
29/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
30/// let mut count = 0;
31/// for quad in NQuadsParser::new().for_reader(file.as_bytes()) {
32///     let quad = quad?;
33///     if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
34///         count += 1;
35///     }
36/// }
37/// assert_eq!(2, count);
38/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
39/// ```
40#[derive(Default, Clone)]
41#[must_use]
42pub struct NQuadsParser {
43    lenient: bool,
44}
45
46impl NQuadsParser {
47    /// Builds a new [`NQuadsParser`].
48    #[inline]
49    pub fn new() -> Self {
50        Self::default()
51    }
52
53    /// Assumes the file is valid to make parsing faster.
54    ///
55    /// It will skip some validations.
56    ///
57    /// Note that if the file is actually not valid, the parser might emit broken RDF.
58    #[inline]
59    pub fn lenient(mut self) -> Self {
60        self.lenient = true;
61        self
62    }
63
64    #[deprecated(note = "Use `lenient()` instead", since = "0.2.0")]
65    #[inline]
66    pub fn unchecked(self) -> Self {
67        self.lenient()
68    }
69
70    /// Parses a N-Quads file from a [`Read`] implementation.
71    ///
72    /// Count the number of people:
73    /// ```
74    /// use oxrdf::{NamedNodeRef, vocab::rdf};
75    /// use oxttl::NQuadsParser;
76    ///
77    /// let file = r#"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
78    /// <http://example.com/foo> <http://schema.org/name> "Foo" .
79    /// <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
80    /// <http://example.com/bar> <http://schema.org/name> "Bar" ."#;
81    ///
82    /// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
83    /// let mut count = 0;
84    /// for quad in NQuadsParser::new().for_reader(file.as_bytes()) {
85    ///     let quad = quad?;
86    ///     if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
87    ///         count += 1;
88    ///     }
89    /// }
90    /// assert_eq!(2, count);
91    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
92    /// ```
93    pub fn for_reader<R: Read>(self, reader: R) -> ReaderNQuadsParser<R> {
94        ReaderNQuadsParser {
95            inner: self.low_level().parser.for_reader(reader),
96        }
97    }
98
99    /// Parses a N-Quads file from a [`AsyncRead`] implementation.
100    ///
101    /// Count the number of people:
102    /// ```
103    /// # #[tokio::main(flavor = "current_thread")]
104    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
105    /// use oxrdf::{NamedNodeRef, vocab::rdf};
106    /// use oxttl::NQuadsParser;
107    ///
108    /// let file = r#"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
109    /// <http://example.com/foo> <http://schema.org/name> "Foo" .
110    /// <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
111    /// <http://example.com/bar> <http://schema.org/name> "Bar" ."#;
112    ///
113    /// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
114    /// let mut count = 0;
115    /// let mut parser = NQuadsParser::new().for_tokio_async_reader(file.as_bytes());
116    /// while let Some(triple) = parser.next().await {
117    ///     let triple = triple?;
118    ///     if triple.predicate == rdf::TYPE && triple.object == schema_person.into() {
119    ///         count += 1;
120    ///     }
121    /// }
122    /// assert_eq!(2, count);
123    /// # Ok(())
124    /// # }
125    /// ```
126    #[cfg(feature = "async-tokio")]
127    pub fn for_tokio_async_reader<R: AsyncRead + Unpin>(
128        self,
129        reader: R,
130    ) -> TokioAsyncReaderNQuadsParser<R> {
131        TokioAsyncReaderNQuadsParser {
132            inner: self.low_level().parser.for_tokio_async_reader(reader),
133        }
134    }
135
136    /// Parses a N-Quads file from a byte slice.
137    ///
138    /// Count the number of people:
139    /// ```
140    /// use oxrdf::{NamedNodeRef, vocab::rdf};
141    /// use oxttl::NQuadsParser;
142    ///
143    /// let file = r#"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
144    /// <http://example.com/foo> <http://schema.org/name> "Foo" .
145    /// <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
146    /// <http://example.com/bar> <http://schema.org/name> "Bar" ."#;
147    ///
148    /// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
149    /// let mut count = 0;
150    /// for quad in NQuadsParser::new().for_slice(file) {
151    ///     let quad = quad?;
152    ///     if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
153    ///         count += 1;
154    ///     }
155    /// }
156    /// assert_eq!(2, count);
157    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
158    /// ```
159    pub fn for_slice(self, slice: &(impl AsRef<[u8]> + ?Sized)) -> SliceNQuadsParser<'_> {
160        SliceNQuadsParser {
161            inner: NQuadsRecognizer::new_parser(slice.as_ref(), true, true, self.lenient)
162                .into_iter(),
163        }
164    }
165
166    /// Creates a vector of parsers that may be used to parse an NQuads document slice in parallel.
167    /// To dynamically specify target_parallelism, use e.g. [`std::thread::available_parallelism`].
168    /// Intended to work on large documents.
169    ///
170    /// Count the number of people:
171    /// ```
172    /// use oxrdf::vocab::rdf;
173    /// use oxrdf::NamedNodeRef;
174    /// use oxttl::NQuadsParser;
175    /// use rayon::iter::{IntoParallelIterator, ParallelIterator};
176    ///
177    /// let file = r#"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
178    /// <http://example.com/foo> <http://schema.org/name> "Foo" .
179    /// <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
180    /// <http://example.com/bar> <http://schema.org/name> "Bar" ."#;
181    ///
182    /// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
183    /// let readers = NQuadsParser::new().split_slice_for_parallel_parsing(file, 2);
184    /// let count = readers
185    ///     .into_par_iter()
186    ///     .map(|reader| {
187    ///         let mut count = 0;
188    ///         for quad in reader {
189    ///             let quad = quad.unwrap();
190    ///             if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
191    ///                 count += 1;
192    ///             }
193    ///         }
194    ///         count
195    ///     })
196    ///     .sum();
197    /// assert_eq!(2, count);
198    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
199    /// ```
200    pub fn split_slice_for_parallel_parsing(
201        self,
202        slice: &(impl AsRef<[u8]> + ?Sized),
203        target_parallelism: usize,
204    ) -> Vec<SliceNQuadsParser<'_>> {
205        let slice = slice.as_ref();
206        let n_chunks = (slice.len() / MIN_PARALLEL_CHUNK_SIZE).clamp(1, target_parallelism);
207        get_ntriples_slice_chunks(slice, n_chunks)
208            .into_iter()
209            .map(|(start, end)| self.clone().for_slice(&slice[start..end]))
210            .collect()
211    }
212
213    /// Creates a vector of parsers that may be used to parse an NQuads file in parallel.
214    /// To dynamically specify target_parallelism, use e.g. [`std::thread::available_parallelism`].
215    /// Intended to work on large documents.
216    ///
217    /// Count the number of people:
218    /// ```no_run
219    /// use oxrdf::vocab::rdf;
220    /// use oxrdf::NamedNodeRef;
221    /// use oxttl::NTriplesParser;
222    /// use rayon::iter::{IntoParallelIterator, ParallelIterator};
223    /// # let path = tempfile::NamedTempFile::new()?;
224    /// # std::fs::write(&path, r#"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
225    /// # <http://example.com/foo> <http://schema.org/name> "Foo" .
226    /// # <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
227    /// # <http://example.com/bar> <http://schema.org/name> "Bar" ."#)?;
228    ///
229    /// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
230    /// let readers = NTriplesParser::new().split_file_for_parallel_parsing(&path, 2)?;
231    /// let count = readers
232    ///     .into_par_iter()
233    ///     .map(|reader| {
234    ///         let mut count = 0;
235    ///         for triple in reader {
236    ///             let triple = triple.unwrap();
237    ///             if triple.predicate == rdf::TYPE && triple.object == schema_person.into() {
238    ///                 count += 1;
239    ///             }
240    ///         }
241    ///         count
242    ///     })
243    ///     .sum();
244    /// assert_eq!(2, count);
245    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
246    /// ```
247    pub fn split_file_for_parallel_parsing(
248        self,
249        path: impl AsRef<Path>,
250        target_parallelism: usize,
251    ) -> io::Result<Vec<ReaderNQuadsParser<Take<File>>>> {
252        let path = path.as_ref();
253        let mut file = File::open(path)?;
254        let file_size = file.metadata()?.len();
255        let n_chunks = usize::try_from(
256            file_size / u64::try_from(MIN_PARALLEL_CHUNK_SIZE).map_err(io::Error::other)?,
257        )
258        .map_err(io::Error::other)?
259        .clamp(1, target_parallelism);
260        get_ntriples_file_chunks(&mut file, file_size, n_chunks)?
261            .into_iter()
262            .map(|(start, end)| {
263                let mut file = File::open(path)?;
264                file.seek(SeekFrom::Start(start))?;
265                Ok(self.clone().for_reader(file.take(end - start)))
266            })
267            .collect()
268    }
269
270    /// Allows parsing an N-Quads file by using a low-level API.
271    ///
272    /// Count the number of people:
273    /// ```
274    /// use oxrdf::{NamedNodeRef, vocab::rdf};
275    /// use oxttl::NQuadsParser;
276    ///
277    /// let file: [&[u8]; 4] = [
278    ///     b"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .\n",
279    ///     b"<http://example.com/foo> <http://schema.org/name> \"Foo\" .\n",
280    ///     b"<http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .\n",
281    ///     b"<http://example.com/bar> <http://schema.org/name> \"Bar\" .\n"
282    /// ];
283    ///
284    /// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
285    /// let mut count = 0;
286    /// let mut parser = NQuadsParser::new().low_level();
287    /// let mut file_chunks = file.iter();
288    /// while !parser.is_end() {
289    ///     // We feed more data to the parser
290    ///     if let Some(chunk) = file_chunks.next() {
291    ///         parser.extend_from_slice(chunk);    
292    ///     } else {
293    ///         parser.end(); // It's finished
294    ///     }
295    ///     // We read as many quads from the parser as possible
296    ///     while let Some(quad) = parser.parse_next() {
297    ///         let quad = quad?;
298    ///         if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
299    ///             count += 1;
300    ///         }
301    ///     }
302    /// }
303    /// assert_eq!(2, count);
304    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
305    /// ```
306    pub fn low_level(self) -> LowLevelNQuadsParser {
307        LowLevelNQuadsParser {
308            parser: NQuadsRecognizer::new_parser(Vec::new(), false, true, self.lenient),
309        }
310    }
311}
312
313/// Parses a N-Quads file from a [`Read`] implementation.
314///
315/// Can be built using [`NQuadsParser::for_reader`].
316///
317/// Count the number of people:
318/// ```
319/// use oxrdf::{NamedNodeRef, vocab::rdf};
320/// use oxttl::NQuadsParser;
321///
322/// let file = r#"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
323/// <http://example.com/foo> <http://schema.org/name> "Foo" .
324/// <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
325/// <http://example.com/bar> <http://schema.org/name> "Bar" ."#;
326///
327/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
328/// let mut count = 0;
329/// for quad in NQuadsParser::new().for_reader(file.as_bytes()) {
330///     let quad = quad?;
331///     if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
332///         count += 1;
333///     }
334/// }
335/// assert_eq!(2, count);
336/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
337/// ```
338#[must_use]
339pub struct ReaderNQuadsParser<R: Read> {
340    inner: ReaderIterator<R, NQuadsRecognizer>,
341}
342
343impl<R: Read> Iterator for ReaderNQuadsParser<R> {
344    type Item = Result<Quad, TurtleParseError>;
345
346    fn next(&mut self) -> Option<Self::Item> {
347        self.inner.next()
348    }
349}
350
351/// Parses a N-Quads file from a [`AsyncRead`] implementation.
352///
353/// Can be built using [`NQuadsParser::for_tokio_async_reader`].
354///
355/// Count the number of people:
356/// ```
357/// # #[tokio::main(flavor = "current_thread")]
358/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
359/// use oxrdf::{NamedNodeRef, vocab::rdf};
360/// use oxttl::NQuadsParser;
361///
362/// let file = r#"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
363/// <http://example.com/foo> <http://schema.org/name> "Foo" .
364/// <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
365/// <http://example.com/bar> <http://schema.org/name> "Bar" ."#;
366///
367/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
368/// let mut count = 0;
369/// let mut parser = NQuadsParser::new().for_tokio_async_reader(file.as_bytes());
370/// while let Some(triple) = parser.next().await {
371///     let triple = triple?;
372///     if triple.predicate == rdf::TYPE && triple.object == schema_person.into() {
373///         count += 1;
374///     }
375/// }
376/// assert_eq!(2, count);
377/// # Ok(())
378/// # }
379/// ```
380#[cfg(feature = "async-tokio")]
381#[must_use]
382pub struct TokioAsyncReaderNQuadsParser<R: AsyncRead + Unpin> {
383    inner: TokioAsyncReaderIterator<R, NQuadsRecognizer>,
384}
385
386#[cfg(feature = "async-tokio")]
387impl<R: AsyncRead + Unpin> TokioAsyncReaderNQuadsParser<R> {
388    /// Reads the next triple or returns `None` if the file is finished.
389    pub async fn next(&mut self) -> Option<Result<Quad, TurtleParseError>> {
390        self.inner.next().await
391    }
392}
393
394/// Parses an N-Quads file from a byte slice.
395///
396/// Can be built using [`NQuadsParser::for_slice`].
397///
398/// Count the number of people:
399/// ```
400/// use oxrdf::{NamedNodeRef, vocab::rdf};
401/// use oxttl::NQuadsParser;
402///
403/// let file = r#"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
404/// <http://example.com/foo> <http://schema.org/name> "Foo" .
405/// <http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .
406/// <http://example.com/bar> <http://schema.org/name> "Bar" ."#;
407///
408/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
409/// let mut count = 0;
410/// for quad in NQuadsParser::new().for_slice(file) {
411///     let quad = quad?;
412///     if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
413///         count += 1;
414///     }
415/// }
416/// assert_eq!(2, count);
417/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
418/// ```
419#[must_use]
420pub struct SliceNQuadsParser<'a> {
421    inner: SliceIterator<'a, NQuadsRecognizer>,
422}
423
424impl Iterator for SliceNQuadsParser<'_> {
425    type Item = Result<Quad, TurtleSyntaxError>;
426
427    fn next(&mut self) -> Option<Self::Item> {
428        self.inner.next()
429    }
430}
431
432/// Parses a N-Quads file by using a low-level API.
433///
434/// Can be built using [`NQuadsParser::low_level`].
435///
436/// Count the number of people:
437/// ```
438/// use oxrdf::{NamedNodeRef, vocab::rdf};
439/// use oxttl::NQuadsParser;
440///
441/// let file: [&[u8]; 4] = [
442///     b"<http://example.com/foo> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .\n",
443///     b"<http://example.com/foo> <http://schema.org/name> \"Foo\" .\n",
444///     b"<http://example.com/bar> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .\n",
445///     b"<http://example.com/bar> <http://schema.org/name> \"Bar\" .\n"
446/// ];
447///
448/// let schema_person = NamedNodeRef::new("http://schema.org/Person")?;
449/// let mut count = 0;
450/// let mut parser = NQuadsParser::new().low_level();
451/// let mut file_chunks = file.iter();
452/// while !parser.is_end() {
453///     // We feed more data to the parser
454///     if let Some(chunk) = file_chunks.next() {
455///         parser.extend_from_slice(chunk);    
456///     } else {
457///         parser.end(); // It's finished
458///     }
459///     // We read as many quads from the parser as possible
460///     while let Some(quad) = parser.parse_next() {
461///         let quad = quad?;
462///         if quad.predicate == rdf::TYPE && quad.object == schema_person.into() {
463///             count += 1;
464///         }
465///     }
466/// }
467/// assert_eq!(2, count);
468/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
469/// ```
470pub struct LowLevelNQuadsParser {
471    parser: Parser<Vec<u8>, NQuadsRecognizer>,
472}
473
474impl LowLevelNQuadsParser {
475    /// Adds some extra bytes to the parser. Should be called when [`parse_next`](Self::parse_next) returns [`None`] and there is still unread data.
476    pub fn extend_from_slice(&mut self, other: &[u8]) {
477        self.parser.extend_from_slice(other)
478    }
479
480    /// Tell the parser that the file is finished.
481    ///
482    /// This triggers the parsing of the final bytes and might lead [`parse_next`](Self::parse_next) to return some extra values.
483    pub fn end(&mut self) {
484        self.parser.end()
485    }
486
487    /// Returns if the parsing is finished i.e. [`end`](Self::end) has been called and [`parse_next`](Self::parse_next) is always going to return `None`.
488    pub fn is_end(&self) -> bool {
489        self.parser.is_end()
490    }
491
492    /// Attempt to parse a new quad from the already provided data.
493    ///
494    /// Returns [`None`] if the parsing is finished or more data is required.
495    /// If it is the case more data should be fed using [`extend_from_slice`](Self::extend_from_slice).
496    pub fn parse_next(&mut self) -> Option<Result<Quad, TurtleSyntaxError>> {
497        self.parser.parse_next()
498    }
499}
500
501/// A [N-Quads](https://www.w3.org/TR/n-quads/) serializer.
502///
503/// ```
504/// use oxrdf::{NamedNodeRef, QuadRef};
505/// use oxrdf::vocab::rdf;
506/// use oxttl::NQuadsSerializer;
507///
508/// let mut serializer = NQuadsSerializer::new().for_writer(Vec::new());
509/// serializer.serialize_quad(QuadRef::new(
510///     NamedNodeRef::new("http://example.com#me")?,
511///     rdf::TYPE,
512///     NamedNodeRef::new("http://schema.org/Person")?,
513///     NamedNodeRef::new("http://example.com")?,
514/// ))?;
515/// assert_eq!(
516///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> <http://example.com> .\n",
517///     serializer.finish().as_slice()
518/// );
519/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
520/// ```
521#[derive(Default, Clone)]
522#[must_use]
523#[expect(clippy::empty_structs_with_brackets)]
524pub struct NQuadsSerializer {}
525
526impl NQuadsSerializer {
527    /// Builds a new [`NQuadsSerializer`].
528    #[inline]
529    pub fn new() -> Self {
530        Self {}
531    }
532
533    /// Writes a N-Quads file to a [`Write`] implementation.
534    ///
535    /// ```
536    /// use oxrdf::{NamedNodeRef, QuadRef};
537    /// use oxrdf::vocab::rdf;
538    /// use oxttl::NQuadsSerializer;
539    ///
540    /// let mut serializer = NQuadsSerializer::new().for_writer(Vec::new());
541    /// serializer.serialize_quad(QuadRef::new(
542    ///     NamedNodeRef::new("http://example.com#me")?,
543    ///     rdf::TYPE,
544    ///     NamedNodeRef::new("http://schema.org/Person")?,
545    ///     NamedNodeRef::new("http://example.com")?,
546    /// ))?;
547    /// assert_eq!(
548    ///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> <http://example.com> .\n",
549    ///     serializer.finish().as_slice()
550    /// );
551    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
552    /// ```
553    pub fn for_writer<W: Write>(self, writer: W) -> WriterNQuadsSerializer<W> {
554        WriterNQuadsSerializer {
555            writer,
556            low_level_writer: self.low_level(),
557        }
558    }
559
560    /// Writes a N-Quads file to a [`AsyncWrite`] implementation.
561    ///
562    /// ```
563    /// # #[tokio::main(flavor = "current_thread")]
564    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
565    /// use oxrdf::{NamedNodeRef, QuadRef};
566    /// use oxttl::NQuadsSerializer;
567    /// use oxrdf::vocab::rdf;
568    ///
569    /// let mut serializer = NQuadsSerializer::new().for_tokio_async_writer(Vec::new());
570    /// serializer.serialize_quad(QuadRef::new(
571    ///     NamedNodeRef::new("http://example.com#me")?,
572    ///     rdf::TYPE,
573    ///     NamedNodeRef::new("http://schema.org/Person")?,
574    ///     NamedNodeRef::new("http://example.com")?,
575    /// )).await?;
576    /// assert_eq!(
577    ///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> <http://example.com> .\n",
578    ///     serializer.finish().as_slice()
579    /// );
580    /// # Ok(())
581    /// # }
582    /// ```
583    #[cfg(feature = "async-tokio")]
584    pub fn for_tokio_async_writer<W: AsyncWrite + Unpin>(
585        self,
586        writer: W,
587    ) -> TokioAsyncWriterNQuadsSerializer<W> {
588        TokioAsyncWriterNQuadsSerializer {
589            writer,
590            low_level_writer: self.low_level(),
591            buffer: Vec::new(),
592        }
593    }
594
595    /// Builds a low-level N-Quads writer.
596    ///
597    /// ```
598    /// use oxrdf::{NamedNodeRef, QuadRef};
599    /// use oxrdf::vocab::rdf;
600    /// use oxttl::NQuadsSerializer;
601    ///
602    /// let mut buf = Vec::new();
603    /// let mut serializer = NQuadsSerializer::new().low_level();
604    /// serializer.serialize_quad(QuadRef::new(
605    ///     NamedNodeRef::new("http://example.com#me")?,
606    ///     rdf::TYPE,
607    ///     NamedNodeRef::new("http://schema.org/Person")?,
608    ///     NamedNodeRef::new("http://example.com")?,
609    /// ), &mut buf)?;
610    /// assert_eq!(
611    ///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> <http://example.com> .\n",
612    ///     buf.as_slice()
613    /// );
614    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
615    /// ```
616    #[expect(clippy::unused_self)]
617    pub fn low_level(self) -> LowLevelNQuadsSerializer {
618        LowLevelNQuadsSerializer {}
619    }
620}
621
622/// Writes a N-Quads file to a [`Write`] implementation.
623///
624/// Can be built using [`NQuadsSerializer::for_writer`].
625///
626/// ```
627/// use oxrdf::{NamedNodeRef, QuadRef};
628/// use oxrdf::vocab::rdf;
629/// use oxttl::NQuadsSerializer;
630///
631/// let mut serializer = NQuadsSerializer::new().for_writer(Vec::new());
632/// serializer.serialize_quad(QuadRef::new(
633///     NamedNodeRef::new("http://example.com#me")?,
634///     rdf::TYPE,
635///     NamedNodeRef::new("http://schema.org/Person")?,
636///     NamedNodeRef::new("http://example.com")?,
637/// ))?;
638/// assert_eq!(
639///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> <http://example.com> .\n",
640///     serializer.finish().as_slice()
641/// );
642/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
643/// ```
644#[must_use]
645pub struct WriterNQuadsSerializer<W: Write> {
646    writer: W,
647    low_level_writer: LowLevelNQuadsSerializer,
648}
649
650impl<W: Write> WriterNQuadsSerializer<W> {
651    /// Writes an extra quad.
652    pub fn serialize_quad<'a>(&mut self, q: impl Into<QuadRef<'a>>) -> io::Result<()> {
653        self.low_level_writer.serialize_quad(q, &mut self.writer)
654    }
655
656    /// Ends the write process and returns the underlying [`Write`].
657    pub fn finish(self) -> W {
658        self.writer
659    }
660}
661
662/// Writes a N-Quads file to a [`AsyncWrite`] implementation.
663///
664/// Can be built using [`NQuadsSerializer::for_tokio_async_writer`].
665///
666/// ```
667/// # #[tokio::main(flavor = "current_thread")]
668/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
669/// use oxrdf::{NamedNodeRef, QuadRef};
670/// use oxrdf::vocab::rdf;
671/// use oxttl::NQuadsSerializer;
672///
673/// let mut serializer = NQuadsSerializer::new().for_tokio_async_writer(Vec::new());
674/// serializer.serialize_quad(QuadRef::new(
675///     NamedNodeRef::new("http://example.com#me")?,
676///     rdf::TYPE,
677///     NamedNodeRef::new("http://schema.org/Person")?,
678///     NamedNodeRef::new("http://example.com")?,
679/// )).await?;
680/// assert_eq!(
681///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> <http://example.com> .\n",
682///     serializer.finish().as_slice()
683/// );
684/// # Ok(())
685/// # }
686/// ```
687#[cfg(feature = "async-tokio")]
688#[must_use]
689pub struct TokioAsyncWriterNQuadsSerializer<W: AsyncWrite + Unpin> {
690    writer: W,
691    low_level_writer: LowLevelNQuadsSerializer,
692    buffer: Vec<u8>,
693}
694
695#[cfg(feature = "async-tokio")]
696impl<W: AsyncWrite + Unpin> TokioAsyncWriterNQuadsSerializer<W> {
697    /// Writes an extra quad.
698    pub async fn serialize_quad<'a>(&mut self, q: impl Into<QuadRef<'a>>) -> io::Result<()> {
699        self.low_level_writer.serialize_quad(q, &mut self.buffer)?;
700        self.writer.write_all(&self.buffer).await?;
701        self.buffer.clear();
702        Ok(())
703    }
704
705    /// Ends the write process and returns the underlying [`Write`].
706    pub fn finish(self) -> W {
707        self.writer
708    }
709}
710
711/// Writes a N-Quads file by using a low-level API.
712///
713/// Can be built using [`NQuadsSerializer::low_level`].
714///
715/// ```
716/// use oxrdf::{NamedNodeRef, QuadRef};
717/// use oxrdf::vocab::rdf;
718/// use oxttl::NQuadsSerializer;
719///
720/// let mut buf = Vec::new();
721/// let mut serializer = NQuadsSerializer::new().low_level();
722/// serializer.serialize_quad(QuadRef::new(
723///     NamedNodeRef::new("http://example.com#me")?,
724///     rdf::TYPE,
725///     NamedNodeRef::new("http://schema.org/Person")?,
726///     NamedNodeRef::new("http://example.com")?,
727/// ), &mut buf)?;
728/// assert_eq!(
729///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> <http://example.com> .\n",
730///     buf.as_slice()
731/// );
732/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
733/// ```
734#[expect(clippy::empty_structs_with_brackets)]
735pub struct LowLevelNQuadsSerializer {}
736
737impl LowLevelNQuadsSerializer {
738    /// Writes an extra quad.
739    #[expect(clippy::unused_self)]
740    pub fn serialize_quad<'a>(
741        &mut self,
742        q: impl Into<QuadRef<'a>>,
743        mut writer: impl Write,
744    ) -> io::Result<()> {
745        writeln!(writer, "{} .", q.into())
746    }
747}