Skip to main content

oxttl/
ntriples.rs

1//! A [N-Triples](https://www.w3.org/TR/n-triples/) streaming parser implemented by [`NTriplesParser`]
2//! and a serializer implemented by [`NTriplesSerializer`].
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::{Triple, TripleRef};
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-Triples](https://www.w3.org/TR/n-triples/) streaming parser.
18///
19/// Count the number of people:
20/// ```
21/// use oxrdf::{NamedNodeRef, vocab::rdf};
22/// use oxttl::NTriplesParser;
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 triple in NTriplesParser::new().for_reader(file.as_bytes()) {
32///     let triple = triple?;
33///     if triple.predicate == rdf::TYPE && triple.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 NTriplesParser {
43    lenient: bool,
44}
45
46impl NTriplesParser {
47    /// Builds a new [`NTriplesParser`].
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-Triples file from a [`Read`] implementation.
71    ///
72    /// Count the number of people:
73    /// ```
74    /// use oxrdf::{NamedNodeRef, vocab::rdf};
75    /// use oxttl::NTriplesParser;
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 triple in NTriplesParser::new().for_reader(file.as_bytes()) {
85    ///     let triple = triple?;
86    ///     if triple.predicate == rdf::TYPE && triple.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) -> ReaderNTriplesParser<R> {
94        ReaderNTriplesParser {
95            inner: self.low_level().parser.for_reader(reader),
96        }
97    }
98
99    /// Parses a N-Triples 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::NTriplesParser;
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 = NTriplesParser::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    ) -> TokioAsyncReaderNTriplesParser<R> {
131        TokioAsyncReaderNTriplesParser {
132            inner: self.low_level().parser.for_tokio_async_reader(reader),
133        }
134    }
135
136    /// Parses a N-Triples file from a byte slice.
137    ///
138    /// Count the number of people:
139    /// ```
140    /// use oxrdf::{NamedNodeRef, vocab::rdf};
141    /// use oxttl::NTriplesParser;
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 triple in NTriplesParser::new().for_slice(file) {
151    ///     let triple = triple?;
152    ///     if triple.predicate == rdf::TYPE && triple.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)) -> SliceNTriplesParser<'_> {
160        SliceNTriplesParser {
161            inner: NQuadsRecognizer::new_parser(slice.as_ref(), true, false, self.lenient)
162                .into_iter(),
163        }
164    }
165
166    /// Creates a vector of parsers that may be used to parse an NTriples 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::{NTriplesParser};
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 = NTriplesParser::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 triple in reader {
189    ///             let triple = triple.unwrap();
190    ///             if triple.predicate == rdf::TYPE && triple.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<SliceNTriplesParser<'_>> {
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 NTriples 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<ReaderNTriplesParser<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-Triples file by using a low-level API.
271    ///
272    /// Count the number of people:
273    /// ```
274    /// use oxrdf::{NamedNodeRef, vocab::rdf};
275    /// use oxttl::NTriplesParser;
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 = NTriplesParser::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 triples from the parser as possible
296    ///     while let Some(triple) = parser.parse_next() {
297    ///         let triple = triple?;
298    ///         if triple.predicate == rdf::TYPE && triple.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) -> LowLevelNTriplesParser {
307        LowLevelNTriplesParser {
308            parser: NQuadsRecognizer::new_parser(Vec::new(), false, false, self.lenient),
309        }
310    }
311}
312
313/// Parses a N-Triples file from a [`Read`] implementation.
314///
315/// Can be built using [`NTriplesParser::for_reader`].
316///
317/// Count the number of people:
318/// ```
319/// use oxrdf::{NamedNodeRef, vocab::rdf};
320/// use oxttl::NTriplesParser;
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 triple in NTriplesParser::new().for_reader(file.as_bytes()) {
330///     let triple = triple?;
331///     if triple.predicate == rdf::TYPE && triple.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 ReaderNTriplesParser<R: Read> {
340    inner: ReaderIterator<R, NQuadsRecognizer>,
341}
342
343impl<R: Read> Iterator for ReaderNTriplesParser<R> {
344    type Item = Result<Triple, TurtleParseError>;
345
346    fn next(&mut self) -> Option<Self::Item> {
347        Some(self.inner.next()?.map(Into::into))
348    }
349}
350
351/// Parses a N-Triples file from a [`AsyncRead`] implementation.
352///
353/// Can be built using [`NTriplesParser::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::NTriplesParser;
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 = NTriplesParser::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 TokioAsyncReaderNTriplesParser<R: AsyncRead + Unpin> {
383    inner: TokioAsyncReaderIterator<R, NQuadsRecognizer>,
384}
385
386#[cfg(feature = "async-tokio")]
387impl<R: AsyncRead + Unpin> TokioAsyncReaderNTriplesParser<R> {
388    /// Reads the next triple or returns `None` if the file is finished.
389    pub async fn next(&mut self) -> Option<Result<Triple, TurtleParseError>> {
390        Some(self.inner.next().await?.map(Into::into))
391    }
392}
393
394/// Parses an N-Triples file from a byte slice.
395///
396/// Can be built using [`NTriplesParser::for_slice`].
397///
398/// Count the number of people:
399/// ```
400/// use oxrdf::{NamedNodeRef, vocab::rdf};
401/// use oxttl::NTriplesParser;
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 triple in NTriplesParser::new().for_slice(file) {
411///     let triple = triple?;
412///     if triple.predicate == rdf::TYPE && triple.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 SliceNTriplesParser<'a> {
421    inner: SliceIterator<'a, NQuadsRecognizer>,
422}
423
424impl Iterator for SliceNTriplesParser<'_> {
425    type Item = Result<Triple, TurtleSyntaxError>;
426
427    fn next(&mut self) -> Option<Self::Item> {
428        Some(self.inner.next()?.map(Into::into))
429    }
430}
431
432/// Parses a N-Triples file by using a low-level API.
433///
434/// Can be built using [`NTriplesParser::low_level`].
435///
436/// Count the number of people:
437/// ```
438/// use oxrdf::{NamedNodeRef, vocab::rdf};
439/// use oxttl::NTriplesParser;
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 = NTriplesParser::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 triples from the parser as possible
460///     while let Some(triple) = parser.parse_next() {
461///         let triple = triple?;
462///         if triple.predicate == rdf::TYPE && triple.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 LowLevelNTriplesParser {
471    parser: Parser<Vec<u8>, NQuadsRecognizer>,
472}
473
474impl LowLevelNTriplesParser {
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 triple 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<Triple, TurtleSyntaxError>> {
497        Some(self.parser.parse_next()?.map(Into::into))
498    }
499}
500
501/// A [canonical](https://www.w3.org/TR/n-triples/#canonical-ntriples) [N-Triples](https://www.w3.org/TR/n-triples/) serializer.
502///
503/// ```
504/// use oxrdf::{NamedNodeRef, TripleRef};
505/// use oxrdf::vocab::rdf;
506/// use oxttl::NTriplesSerializer;
507///
508/// let mut serializer = NTriplesSerializer::new().for_writer(Vec::new());
509/// serializer.serialize_triple(TripleRef::new(
510///     NamedNodeRef::new("http://example.com#me")?,
511///     rdf::TYPE,
512///     NamedNodeRef::new("http://schema.org/Person")?,
513/// ))?;
514/// assert_eq!(
515///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .\n",
516///     serializer.finish().as_slice()
517/// );
518/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
519/// ```
520#[derive(Default, Clone)]
521#[must_use]
522#[expect(clippy::empty_structs_with_brackets)]
523pub struct NTriplesSerializer {}
524
525impl NTriplesSerializer {
526    /// Builds a new [`NTriplesSerializer`].
527    #[inline]
528    pub fn new() -> Self {
529        Self {}
530    }
531
532    /// Writes a N-Triples file to a [`Write`] implementation.
533    ///
534    /// ```
535    /// use oxrdf::{NamedNodeRef, TripleRef};
536    /// use oxrdf::vocab::rdf;
537    /// use oxttl::NTriplesSerializer;
538    ///
539    /// let mut serializer = NTriplesSerializer::new().for_writer(Vec::new());
540    /// serializer.serialize_triple(TripleRef::new(
541    ///     NamedNodeRef::new("http://example.com#me")?,
542    ///     rdf::TYPE,
543    ///     NamedNodeRef::new("http://schema.org/Person")?,
544    /// ))?;
545    /// assert_eq!(
546    ///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .\n",
547    ///     serializer.finish().as_slice()
548    /// );
549    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
550    /// ```
551    pub fn for_writer<W: Write>(self, writer: W) -> WriterNTriplesSerializer<W> {
552        WriterNTriplesSerializer {
553            writer,
554            low_level_writer: self.low_level(),
555        }
556    }
557
558    /// Writes a N-Triples file to a [`AsyncWrite`] implementation.
559    ///
560    /// ```
561    /// # #[tokio::main(flavor = "current_thread")]
562    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
563    /// use oxrdf::{NamedNodeRef, TripleRef};
564    /// use oxrdf::vocab::rdf;
565    /// use oxttl::NTriplesSerializer;
566    ///
567    /// let mut serializer = NTriplesSerializer::new().for_tokio_async_writer(Vec::new());
568    /// serializer.serialize_triple(TripleRef::new(
569    ///     NamedNodeRef::new("http://example.com#me")?,
570    ///     rdf::TYPE,
571    ///     NamedNodeRef::new("http://schema.org/Person")?,
572    /// )).await?;
573    /// assert_eq!(
574    ///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .\n",
575    ///     serializer.finish().as_slice()
576    /// );
577    /// # Ok(())
578    /// # }
579    /// ```
580    #[cfg(feature = "async-tokio")]
581    pub fn for_tokio_async_writer<W: AsyncWrite + Unpin>(
582        self,
583        writer: W,
584    ) -> TokioAsyncWriterNTriplesSerializer<W> {
585        TokioAsyncWriterNTriplesSerializer {
586            writer,
587            low_level_writer: self.low_level(),
588            buffer: Vec::new(),
589        }
590    }
591
592    /// Builds a low-level N-Triples writer.
593    ///
594    /// ```
595    /// use oxrdf::{NamedNodeRef, TripleRef};
596    /// use oxrdf::vocab::rdf;
597    /// use oxttl::NTriplesSerializer;
598    ///
599    /// let mut buf = Vec::new();
600    /// let mut serializer = NTriplesSerializer::new().low_level();
601    /// serializer.serialize_triple(TripleRef::new(
602    ///     NamedNodeRef::new("http://example.com#me")?,
603    ///     rdf::TYPE,
604    ///     NamedNodeRef::new("http://schema.org/Person")?,
605    /// ), &mut buf)?;
606    /// assert_eq!(
607    ///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .\n",
608    ///     buf.as_slice()
609    /// );
610    /// # Result::<_, Box<dyn std::error::Error>>::Ok(())
611    /// ```
612    #[expect(clippy::unused_self)]
613    pub fn low_level(self) -> LowLevelNTriplesSerializer {
614        LowLevelNTriplesSerializer {}
615    }
616}
617
618/// Writes a N-Triples file to a [`Write`] implementation.
619///
620/// Can be built using [`NTriplesSerializer::for_writer`].
621///
622/// ```
623/// use oxrdf::{NamedNodeRef, TripleRef};
624/// use oxrdf::vocab::rdf;
625/// use oxttl::NTriplesSerializer;
626///
627/// let mut serializer = NTriplesSerializer::new().for_writer(Vec::new());
628/// serializer.serialize_triple(TripleRef::new(
629///     NamedNodeRef::new("http://example.com#me")?,
630///     rdf::TYPE,
631///     NamedNodeRef::new("http://schema.org/Person")?,
632/// ))?;
633/// assert_eq!(
634///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .\n",
635///     serializer.finish().as_slice()
636/// );
637/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
638/// ```
639#[must_use]
640pub struct WriterNTriplesSerializer<W: Write> {
641    writer: W,
642    low_level_writer: LowLevelNTriplesSerializer,
643}
644
645impl<W: Write> WriterNTriplesSerializer<W> {
646    /// Writes an extra triple.
647    pub fn serialize_triple<'a>(&mut self, t: impl Into<TripleRef<'a>>) -> io::Result<()> {
648        self.low_level_writer.serialize_triple(t, &mut self.writer)
649    }
650
651    /// Ends the write process and returns the underlying [`Write`].
652    pub fn finish(self) -> W {
653        self.writer
654    }
655}
656
657/// Writes a N-Triples file to a [`AsyncWrite`] implementation.
658///
659/// Can be built using [`NTriplesSerializer::for_tokio_async_writer`].
660///
661/// ```
662/// # #[tokio::main(flavor = "current_thread")]
663/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
664/// use oxrdf::{NamedNodeRef, TripleRef};
665/// use oxrdf::vocab::rdf;
666/// use oxttl::NTriplesSerializer;
667///
668/// let mut serializer = NTriplesSerializer::new().for_tokio_async_writer(Vec::new());
669/// serializer.serialize_triple(TripleRef::new(
670///     NamedNodeRef::new("http://example.com#me")?,
671///     rdf::TYPE,
672///     NamedNodeRef::new("http://schema.org/Person")?
673/// )).await?;
674/// assert_eq!(
675///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .\n",
676///     serializer.finish().as_slice()
677/// );
678/// # Ok(())
679/// # }
680/// ```
681#[cfg(feature = "async-tokio")]
682#[must_use]
683pub struct TokioAsyncWriterNTriplesSerializer<W: AsyncWrite + Unpin> {
684    writer: W,
685    low_level_writer: LowLevelNTriplesSerializer,
686    buffer: Vec<u8>,
687}
688
689#[cfg(feature = "async-tokio")]
690impl<W: AsyncWrite + Unpin> TokioAsyncWriterNTriplesSerializer<W> {
691    /// Writes an extra triple.
692    pub async fn serialize_triple<'a>(&mut self, t: impl Into<TripleRef<'a>>) -> io::Result<()> {
693        self.low_level_writer
694            .serialize_triple(t, &mut self.buffer)?;
695        self.writer.write_all(&self.buffer).await?;
696        self.buffer.clear();
697        Ok(())
698    }
699
700    /// Ends the write process and returns the underlying [`Write`].
701    pub fn finish(self) -> W {
702        self.writer
703    }
704}
705
706/// Writes a N-Triples file by using a low-level API.
707///
708/// Can be built using [`NTriplesSerializer::low_level`].
709///
710/// ```
711/// use oxrdf::{NamedNodeRef, TripleRef};
712/// use oxrdf::vocab::rdf;
713/// use oxttl::NTriplesSerializer;
714///
715/// let mut buf = Vec::new();
716/// let mut serializer = NTriplesSerializer::new().low_level();
717/// serializer.serialize_triple(TripleRef::new(
718///     NamedNodeRef::new("http://example.com#me")?,
719///     rdf::TYPE,
720///     NamedNodeRef::new("http://schema.org/Person")?,
721/// ), &mut buf)?;
722/// assert_eq!(
723///     b"<http://example.com#me> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Person> .\n",
724///     buf.as_slice()
725/// );
726/// # Result::<_, Box<dyn std::error::Error>>::Ok(())
727/// ```
728#[expect(clippy::empty_structs_with_brackets)]
729pub struct LowLevelNTriplesSerializer {}
730
731impl LowLevelNTriplesSerializer {
732    /// Writes an extra triple.
733    #[expect(clippy::unused_self)]
734    pub fn serialize_triple<'a>(
735        &mut self,
736        t: impl Into<TripleRef<'a>>,
737        mut writer: impl Write,
738    ) -> io::Result<()> {
739        writeln!(writer, "{} .", t.into())
740    }
741}
742
743#[cfg(test)]
744mod tests {
745    use super::*;
746    use oxrdf::{Literal, NamedNode};
747
748    #[test]
749    fn lenient_parsing() {
750        let triples = NTriplesParser::new()
751            .lenient()
752            .for_reader(r#"<foo> <bar> "baz"@toolonglangtag ."#.as_bytes())
753            .collect::<Result<Vec<_>, _>>()
754            .unwrap();
755        assert_eq!(
756            triples,
757            [Triple::new(
758                NamedNode::new_unchecked("foo"),
759                NamedNode::new_unchecked("bar"),
760                Literal::new_language_tagged_literal_unchecked("baz", "toolonglangtag"),
761            )]
762        )
763    }
764}