Skip to main content

oxirs_core/format/
ntriples.rs

1//! N-Triples Format Parser and Serializer
2//!
3//! Extracted and adapted from OxiGraph oxttl with OxiRS enhancements.
4//! Based on W3C N-Triples specification: <https://www.w3.org/TR/n-triples/>
5
6use super::error::SerializeResult;
7use super::error::{ParseResult, RdfParseError, TextPosition};
8use super::serializer::QuadSerializer;
9use crate::model::{BlankNode, Literal, NamedNode, Triple, TripleRef};
10use std::io::{Read, Write};
11
12/// Represents a parsed N-Triples term
13#[derive(Debug, Clone)]
14enum NTriplesTerm {
15    Iri(String),
16    BlankNode(String),
17    Literal(String),
18    LanguageLiteral(String, String),
19    TypedLiteral(String, String),
20}
21
22/// N-Triples parser implementation
23#[derive(Debug, Clone)]
24pub struct NTriplesParser {
25    lenient: bool,
26}
27
28impl NTriplesParser {
29    /// Create a new N-Triples parser
30    pub fn new() -> Self {
31        Self { lenient: false }
32    }
33
34    /// Enable lenient parsing (skip some validations)
35    pub fn lenient(mut self) -> Self {
36        self.lenient = true;
37        self
38    }
39
40    /// Parse N-Triples from a reader
41    pub fn parse_reader<R: Read>(&self, mut reader: R) -> ParseResult<Vec<Triple>> {
42        // Read all data from the reader
43        let mut buffer = String::new();
44        reader.read_to_string(&mut buffer)?;
45
46        // Use the string parser
47        self.parse_str(&buffer)
48    }
49
50    /// Parse N-Triples from a byte slice
51    pub fn parse_slice(&self, slice: &[u8]) -> ParseResult<Vec<Triple>> {
52        let content = std::str::from_utf8(slice)
53            .map_err(|e| RdfParseError::syntax(format!("Invalid UTF-8: {e}")))?;
54        self.parse_str(content)
55    }
56
57    /// Parse N-Triples from a string
58    pub fn parse_str(&self, input: &str) -> ParseResult<Vec<Triple>> {
59        let mut triples = Vec::new();
60        let mut line_number = 1;
61
62        for line in input.lines() {
63            let trimmed = line.trim();
64
65            // Skip empty lines and comments
66            if trimmed.is_empty() || trimmed.starts_with('#') {
67                line_number += 1;
68                continue;
69            }
70
71            // Parse triple
72            match self.parse_triple_line(trimmed, line_number) {
73                Ok(Some(triple)) => triples.push(triple),
74                Ok(None) => {} // Valid but empty line
75                Err(e) if self.lenient => {
76                    // Skip invalid lines in lenient mode, routing the
77                    // diagnostic through the crate's tracing logger so
78                    // deployments can filter/route it like any other log.
79                    tracing::warn!("Skipping invalid N-Triples line {line_number}: {e}");
80                }
81                Err(e) => return Err(e),
82            }
83
84            line_number += 1;
85        }
86
87        Ok(triples)
88    }
89
90    /// Parse a single triple line
91    fn parse_triple_line(&self, line: &str, line_number: usize) -> ParseResult<Option<Triple>> {
92        // N-Triples format: <subject> <predicate> <object> .
93        // - Subject: IRI or blank node
94        // - Predicate: IRI
95        // - Object: IRI, blank node, or literal
96
97        // Validate line ends with dot
98        if !line.ends_with('.') {
99            return Err(RdfParseError::syntax_at(
100                "N-Triples line must end with '.'",
101                TextPosition::new(line_number, line.len(), 0),
102            ));
103        }
104
105        // Remove the trailing dot and parse terms
106        let line_without_dot = line[..line.len() - 1].trim();
107
108        // Parse the three terms
109        let mut terms = Vec::new();
110        let mut pos = 0;
111
112        // Parse subject
113        let (subject_term, new_pos) = self.parse_term(line_without_dot, pos, line_number)?;
114        terms.push(subject_term);
115        pos = new_pos;
116
117        // Parse predicate
118        let (predicate_term, new_pos) = self.parse_term(line_without_dot, pos, line_number)?;
119        terms.push(predicate_term);
120        pos = new_pos;
121
122        // Parse object
123        let (object_term, _) = self.parse_term(line_without_dot, pos, line_number)?;
124        terms.push(object_term);
125
126        if terms.len() != 3 {
127            return Err(RdfParseError::syntax_at(
128                "N-Triples line must have exactly 3 terms",
129                TextPosition::new(line_number, 1, 0),
130            ));
131        }
132
133        // Build the triple
134        let subject = self.term_to_subject(&terms[0], line_number)?;
135        let predicate = self.term_to_predicate(&terms[1], line_number)?;
136        let object = self.term_to_object(&terms[2], line_number)?;
137
138        Ok(Some(Triple::new(subject, predicate, object)))
139    }
140
141    /// Check if lenient parsing is enabled
142    pub fn is_lenient(&self) -> bool {
143        self.lenient
144    }
145
146    /// Parse a single term (IRI, blank node, or literal)
147    fn parse_term(
148        &self,
149        input: &str,
150        start_pos: usize,
151        line_number: usize,
152    ) -> ParseResult<(NTriplesTerm, usize)> {
153        let trimmed = input[start_pos..].trim_start();
154        let actual_start = start_pos + (input.len() - start_pos - trimmed.len());
155
156        if trimmed.is_empty() {
157            return Err(RdfParseError::syntax_at(
158                "Expected term but found end of line",
159                TextPosition::new(line_number, actual_start, 0),
160            ));
161        }
162
163        if trimmed.starts_with('<') {
164            // Parse IRI
165            self.parse_iri(trimmed, actual_start, line_number)
166        } else if trimmed.starts_with("_:") {
167            // Parse blank node
168            self.parse_blank_node(trimmed, actual_start, line_number)
169        } else if trimmed.starts_with('"') {
170            // Parse literal
171            self.parse_literal(trimmed, actual_start, line_number)
172        } else {
173            Err(RdfParseError::syntax_at(
174                "Invalid term format. Expected <IRI>, _:blank, or \"literal\"",
175                TextPosition::new(line_number, actual_start, 0),
176            ))
177        }
178    }
179
180    /// Parse an IRI term <...>
181    fn parse_iri(
182        &self,
183        input: &str,
184        start_pos: usize,
185        line_number: usize,
186    ) -> ParseResult<(NTriplesTerm, usize)> {
187        if let Some(end_pos) = input.find('>') {
188            let iri = input[1..end_pos].to_string();
189            let new_pos = start_pos + end_pos + 1;
190            Ok((NTriplesTerm::Iri(iri), new_pos))
191        } else {
192            Err(RdfParseError::syntax_at(
193                "Unterminated IRI - missing '>'",
194                TextPosition::new(line_number, start_pos, 0),
195            ))
196        }
197    }
198
199    /// Parse a blank node term _:...
200    fn parse_blank_node(
201        &self,
202        input: &str,
203        start_pos: usize,
204        line_number: usize,
205    ) -> ParseResult<(NTriplesTerm, usize)> {
206        // Find the end of the blank node ID (whitespace or end)
207        let mut end_pos = 2; // Start after _:
208        for (i, c) in input[2..].char_indices() {
209            if c.is_whitespace() {
210                end_pos = 2 + i;
211                break;
212            }
213            end_pos = 2 + i + c.len_utf8();
214        }
215
216        let blank_id = input[2..end_pos].to_string();
217        if blank_id.is_empty() {
218            return Err(RdfParseError::syntax_at(
219                "Blank node ID cannot be empty",
220                TextPosition::new(line_number, start_pos, 0),
221            ));
222        }
223
224        let new_pos = start_pos + end_pos;
225        Ok((NTriplesTerm::BlankNode(blank_id), new_pos))
226    }
227
228    /// Parse a literal term "..." with optional language tag or datatype
229    fn parse_literal(
230        &self,
231        input: &str,
232        start_pos: usize,
233        line_number: usize,
234    ) -> ParseResult<(NTriplesTerm, usize)> {
235        // Find the closing quote
236        let mut end_quote = None;
237        let mut i = 1; // Start after opening quote
238        let chars: Vec<char> = input.chars().collect();
239
240        while i < chars.len() {
241            if chars[i] == '"' {
242                // Check if it's escaped
243                let mut backslash_count = 0;
244                let mut j = i;
245                while j > 0 && chars[j - 1] == '\\' {
246                    backslash_count += 1;
247                    j -= 1;
248                }
249                if backslash_count % 2 == 0 {
250                    // Even number of backslashes means the quote is not escaped
251                    end_quote = Some(i);
252                    break;
253                }
254            }
255            i += 1;
256        }
257
258        let end_quote = end_quote.ok_or_else(|| {
259            RdfParseError::syntax_at(
260                "Unterminated literal - missing closing quote",
261                TextPosition::new(line_number, start_pos, 0),
262            )
263        })?;
264
265        let literal_value = self.unescape_literal(&input[1..end_quote], line_number, start_pos)?;
266        let mut pos_after_quote = start_pos + end_quote + 1;
267
268        // Check for language tag or datatype
269        let remaining = &input[end_quote + 1..];
270
271        if let Some(stripped) = remaining.strip_prefix('@') {
272            // Language tag
273            let mut lang_end = 1;
274            for (i, c) in stripped.char_indices() {
275                if c.is_whitespace() {
276                    lang_end = 1 + i;
277                    break;
278                }
279                lang_end = 1 + i + c.len_utf8();
280            }
281
282            let language = remaining[1..lang_end].to_string();
283            pos_after_quote = start_pos + end_quote + 1 + lang_end;
284            Ok((
285                NTriplesTerm::LanguageLiteral(literal_value, language),
286                pos_after_quote,
287            ))
288        } else if let Some(stripped) = remaining.strip_prefix("^^") {
289            // Datatype
290            if remaining.len() < 3 || !stripped.starts_with('<') {
291                return Err(RdfParseError::syntax_at(
292                    "Invalid datatype format - expected ^^<datatype>",
293                    TextPosition::new(line_number, pos_after_quote, 0),
294                ));
295            }
296
297            if let Some(datatype_end) = remaining[3..].find('>') {
298                let datatype = remaining[3..3 + datatype_end].to_string();
299                pos_after_quote = start_pos + end_quote + 1 + 3 + datatype_end + 1;
300                Ok((
301                    NTriplesTerm::TypedLiteral(literal_value, datatype),
302                    pos_after_quote,
303                ))
304            } else {
305                Err(RdfParseError::syntax_at(
306                    "Unterminated datatype IRI - missing '>'",
307                    TextPosition::new(line_number, pos_after_quote, 0),
308                ))
309            }
310        } else {
311            // Simple literal
312            Ok((NTriplesTerm::Literal(literal_value), pos_after_quote))
313        }
314    }
315
316    /// Unescape special characters in literal values
317    fn unescape_literal(
318        &self,
319        value: &str,
320        line_number: usize,
321        start_pos: usize,
322    ) -> ParseResult<String> {
323        let mut result = String::new();
324        let mut chars = value.chars();
325
326        while let Some(c) = chars.next() {
327            if c == '\\' {
328                match chars.next() {
329                    Some('"') => result.push('"'),
330                    Some('\\') => result.push('\\'),
331                    Some('n') => result.push('\n'),
332                    Some('r') => result.push('\r'),
333                    Some('t') => result.push('\t'),
334                    Some('u') => {
335                        // Parse \uHHHH Unicode escape
336                        let hex_chars: String = chars.by_ref().take(4).collect();
337                        if hex_chars.len() != 4 {
338                            return Err(RdfParseError::syntax_at(
339                                "Invalid Unicode escape sequence \\uHHHH - expected 4 hex digits",
340                                TextPosition::new(line_number, start_pos, 0),
341                            ));
342                        }
343                        let code_point = u32::from_str_radix(&hex_chars, 16).map_err(|_| {
344                            RdfParseError::syntax_at(
345                                "Invalid hex digits in Unicode escape sequence",
346                                TextPosition::new(line_number, start_pos, 0),
347                            )
348                        })?;
349                        let unicode_char = char::from_u32(code_point).ok_or_else(|| {
350                            RdfParseError::syntax_at(
351                                "Invalid Unicode code point",
352                                TextPosition::new(line_number, start_pos, 0),
353                            )
354                        })?;
355                        result.push(unicode_char);
356                    }
357                    Some('U') => {
358                        // Parse \UHHHHHHHH Unicode escape
359                        let hex_chars: String = chars.by_ref().take(8).collect();
360                        if hex_chars.len() != 8 {
361                            return Err(RdfParseError::syntax_at(
362                                "Invalid Unicode escape sequence \\UHHHHHHHH - expected 8 hex digits",
363                                TextPosition::new(line_number, start_pos, 0),
364                            ));
365                        }
366                        let code_point = u32::from_str_radix(&hex_chars, 16).map_err(|_| {
367                            RdfParseError::syntax_at(
368                                "Invalid hex digits in Unicode escape sequence",
369                                TextPosition::new(line_number, start_pos, 0),
370                            )
371                        })?;
372                        let unicode_char = char::from_u32(code_point).ok_or_else(|| {
373                            RdfParseError::syntax_at(
374                                "Invalid Unicode code point",
375                                TextPosition::new(line_number, start_pos, 0),
376                            )
377                        })?;
378                        result.push(unicode_char);
379                    }
380                    Some(other) => {
381                        return Err(RdfParseError::syntax_at(
382                            format!("Invalid escape sequence \\{other}"),
383                            TextPosition::new(line_number, start_pos, 0),
384                        ));
385                    }
386                    None => {
387                        return Err(RdfParseError::syntax_at(
388                            "Incomplete escape sequence at end of literal",
389                            TextPosition::new(line_number, start_pos, 0),
390                        ));
391                    }
392                }
393            } else {
394                result.push(c);
395            }
396        }
397
398        Ok(result)
399    }
400
401    /// Convert parsed term to Subject
402    fn term_to_subject(
403        &self,
404        term: &NTriplesTerm,
405        line_number: usize,
406    ) -> ParseResult<crate::model::term::Subject> {
407        match term {
408            NTriplesTerm::Iri(iri) => {
409                let named_node = NamedNode::new(iri).map_err(|e| {
410                    RdfParseError::syntax_at(
411                        format!("Invalid subject IRI: {e}"),
412                        TextPosition::new(line_number, 0, 0),
413                    )
414                })?;
415                Ok(crate::model::term::Subject::NamedNode(named_node))
416            }
417            NTriplesTerm::BlankNode(id) => {
418                let blank_node = BlankNode::new(id).map_err(|e| {
419                    RdfParseError::syntax_at(
420                        format!("Invalid blank node: {e}"),
421                        TextPosition::new(line_number, 0, 0),
422                    )
423                })?;
424                Ok(crate::model::term::Subject::BlankNode(blank_node))
425            }
426            _ => Err(RdfParseError::syntax_at(
427                "Subject must be an IRI or blank node",
428                TextPosition::new(line_number, 0, 0),
429            )),
430        }
431    }
432
433    /// Convert parsed term to Predicate
434    fn term_to_predicate(
435        &self,
436        term: &NTriplesTerm,
437        line_number: usize,
438    ) -> ParseResult<crate::model::term::Predicate> {
439        match term {
440            NTriplesTerm::Iri(iri) => {
441                let named_node = NamedNode::new(iri).map_err(|e| {
442                    RdfParseError::syntax_at(
443                        format!("Invalid predicate IRI: {e}"),
444                        TextPosition::new(line_number, 0, 0),
445                    )
446                })?;
447                Ok(crate::model::term::Predicate::NamedNode(named_node))
448            }
449            _ => Err(RdfParseError::syntax_at(
450                "Predicate must be an IRI",
451                TextPosition::new(line_number, 0, 0),
452            )),
453        }
454    }
455
456    /// Convert parsed term to Object
457    fn term_to_object(
458        &self,
459        term: &NTriplesTerm,
460        line_number: usize,
461    ) -> ParseResult<crate::model::term::Object> {
462        match term {
463            NTriplesTerm::Iri(iri) => {
464                let named_node = NamedNode::new(iri).map_err(|e| {
465                    RdfParseError::syntax_at(
466                        format!("Invalid object IRI: {e}"),
467                        TextPosition::new(line_number, 0, 0),
468                    )
469                })?;
470                Ok(crate::model::term::Object::NamedNode(named_node))
471            }
472            NTriplesTerm::BlankNode(id) => {
473                let blank_node = BlankNode::new(id).map_err(|e| {
474                    RdfParseError::syntax_at(
475                        format!("Invalid blank node: {e}"),
476                        TextPosition::new(line_number, 0, 0),
477                    )
478                })?;
479                Ok(crate::model::term::Object::BlankNode(blank_node))
480            }
481            NTriplesTerm::Literal(value) => {
482                let literal = Literal::new(value);
483                Ok(crate::model::term::Object::Literal(literal))
484            }
485            NTriplesTerm::LanguageLiteral(value, lang) => {
486                let literal = Literal::new_language_tagged_literal(value, lang).map_err(|e| {
487                    RdfParseError::syntax_at(
488                        format!("Invalid language tag: {e}"),
489                        TextPosition::new(line_number, 0, 0),
490                    )
491                })?;
492                Ok(crate::model::term::Object::Literal(literal))
493            }
494            NTriplesTerm::TypedLiteral(value, datatype_iri) => {
495                let datatype = NamedNode::new(datatype_iri).map_err(|e| {
496                    RdfParseError::syntax_at(
497                        format!("Invalid datatype IRI: {e}"),
498                        TextPosition::new(line_number, 0, 0),
499                    )
500                })?;
501                let literal = Literal::new_typed_literal(value, datatype);
502                Ok(crate::model::term::Object::Literal(literal))
503            }
504        }
505    }
506}
507
508impl Default for NTriplesParser {
509    fn default() -> Self {
510        Self::new()
511    }
512}
513
514/// N-Triples serializer implementation
515#[derive(Debug, Clone)]
516pub struct NTriplesSerializer {
517    validate: bool,
518}
519
520impl NTriplesSerializer {
521    /// Create a new N-Triples serializer
522    pub fn new() -> Self {
523        Self { validate: true }
524    }
525
526    /// Disable output validation for performance
527    pub fn unvalidated(mut self) -> Self {
528        self.validate = false;
529        self
530    }
531
532    /// Create a writer-based serializer
533    pub fn for_writer<W: Write>(self, writer: W) -> WriterNTriplesSerializer<W> {
534        WriterNTriplesSerializer::new(writer, self)
535    }
536
537    /// Serialize triples to a string
538    pub fn serialize_to_string(&self, triples: &[Triple]) -> SerializeResult<String> {
539        let mut buffer = Vec::new();
540        {
541            let mut serializer = self.clone().for_writer(&mut buffer);
542            for triple in triples {
543                serializer.serialize_triple(triple.as_ref())?;
544            }
545            serializer.finish()?;
546        }
547        String::from_utf8(buffer)
548            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
549    }
550
551    /// Check if validation is enabled
552    pub fn is_validating(&self) -> bool {
553        self.validate
554    }
555}
556
557impl Default for NTriplesSerializer {
558    fn default() -> Self {
559        Self::new()
560    }
561}
562
563/// Writer-based N-Triples serializer
564#[allow(dead_code)]
565pub struct WriterNTriplesSerializer<W: Write> {
566    writer: W,
567    config: NTriplesSerializer,
568}
569
570impl<W: Write> WriterNTriplesSerializer<W> {
571    /// Create a new writer serializer
572    pub fn new(writer: W, config: NTriplesSerializer) -> Self {
573        Self { writer, config }
574    }
575
576    /// Serialize a triple
577    pub fn serialize_triple(&mut self, triple: TripleRef<'_>) -> SerializeResult<()> {
578        // Format: <subject> <predicate> <object> .
579
580        // Serialize subject
581        self.serialize_subject(triple.subject())?;
582        write!(self.writer, " ")?;
583
584        // Serialize predicate
585        self.serialize_predicate(triple.predicate())?;
586        write!(self.writer, " ")?;
587
588        // Serialize object
589        self.serialize_object(triple.object())?;
590        writeln!(self.writer, " .")?;
591
592        Ok(())
593    }
594
595    /// Serialize a subject (IRI or blank node)
596    fn serialize_subject(
597        &mut self,
598        subject: crate::model::triple::SubjectRef<'_>,
599    ) -> SerializeResult<()> {
600        use crate::model::triple::SubjectRef;
601        match subject {
602            SubjectRef::NamedNode(node) => {
603                let escaped_iri = self.escape_iri(node.as_str());
604                write!(self.writer, "<{escaped_iri}>")?;
605            }
606            SubjectRef::BlankNode(node) => {
607                let node_str = node.as_str();
608                write!(self.writer, "_:{node_str}")?;
609            }
610            SubjectRef::Variable(var) => {
611                let var_str = var.as_str();
612                write!(self.writer, "?{var_str}")?;
613            }
614            SubjectRef::QuotedTriple(qt) => {
615                self.serialize_quoted_triple(qt.inner())?;
616            }
617        }
618        Ok(())
619    }
620
621    /// Serialize an RDF-star quoted triple as `<< s p o >>`
622    fn serialize_quoted_triple(&mut self, inner: &crate::model::Triple) -> SerializeResult<()> {
623        write!(self.writer, "<< ")?;
624        self.serialize_subject(inner.subject().into())?;
625        write!(self.writer, " ")?;
626        self.serialize_predicate(inner.predicate().into())?;
627        write!(self.writer, " ")?;
628        self.serialize_object(inner.object().into())?;
629        write!(self.writer, " >>")?;
630        Ok(())
631    }
632
633    /// Serialize a predicate (IRI)
634    fn serialize_predicate(
635        &mut self,
636        predicate: crate::model::triple::PredicateRef<'_>,
637    ) -> SerializeResult<()> {
638        use crate::model::triple::PredicateRef;
639        match predicate {
640            PredicateRef::NamedNode(node) => {
641                let escaped_iri = self.escape_iri(node.as_str());
642                write!(self.writer, "<{escaped_iri}>")?;
643            }
644            PredicateRef::Variable(var) => {
645                let var_str = var.as_str();
646                write!(self.writer, "?{var_str}")?;
647            }
648        }
649        Ok(())
650    }
651
652    /// Serialize an object (IRI, blank node, or literal)
653    fn serialize_object(
654        &mut self,
655        object: crate::model::triple::ObjectRef<'_>,
656    ) -> SerializeResult<()> {
657        use crate::model::triple::ObjectRef;
658        match object {
659            ObjectRef::NamedNode(node) => {
660                let escaped_iri = self.escape_iri(node.as_str());
661                write!(self.writer, "<{escaped_iri}>")?;
662            }
663            ObjectRef::BlankNode(node) => {
664                let node_str = node.as_str();
665                write!(self.writer, "_:{node_str}")?;
666            }
667            ObjectRef::Literal(literal) => {
668                self.serialize_literal(literal)?;
669            }
670            ObjectRef::Variable(var) => {
671                let var_str = var.as_str();
672                write!(self.writer, "?{var_str}")?;
673            }
674            ObjectRef::QuotedTriple(qt) => {
675                self.serialize_quoted_triple(qt.inner())?;
676            }
677        }
678        Ok(())
679    }
680
681    /// Serialize a literal
682    fn serialize_literal(&mut self, literal: &Literal) -> SerializeResult<()> {
683        // Write the literal value with proper escaping
684        let escaped_value = self.escape_literal(literal.value());
685        write!(self.writer, "\"{escaped_value}\"")?;
686
687        // Add language tag or datatype if present
688        if let Some(language) = literal.language() {
689            write!(self.writer, "@{language}")?;
690        } else if literal.datatype().as_str() != crate::vocab::xsd::STRING.as_str() {
691            let escaped_datatype = self.escape_iri(literal.datatype().as_str());
692            write!(self.writer, "^^<{escaped_datatype}>")?;
693        }
694
695        Ok(())
696    }
697
698    /// Escape special characters in IRIs
699    fn escape_iri(&self, iri: &str) -> String {
700        // N-Triples IRIs should already be properly encoded
701        // but we can add basic escaping if needed
702        iri.to_string()
703    }
704
705    /// Escape special characters in literal values
706    fn escape_literal(&self, value: &str) -> String {
707        value
708            .chars()
709            .map(|c| match c {
710                '"' => "\\\"".to_string(),
711                '\\' => "\\\\".to_string(),
712                '\n' => "\\n".to_string(),
713                '\r' => "\\r".to_string(),
714                '\t' => "\\t".to_string(),
715                c if !('\u{0020}'..='\u{007E}').contains(&c) => {
716                    // Escape non-ASCII and control characters
717                    if (c as u32) <= 0xFFFF {
718                        format!("\\u{:04X}", c as u32)
719                    } else {
720                        format!("\\U{:08X}", c as u32)
721                    }
722                }
723                _ => c.to_string(),
724            })
725            .collect()
726    }
727
728    /// Finish serialization and return the writer
729    pub fn finish(self) -> SerializeResult<W> {
730        Ok(self.writer)
731    }
732}
733
734impl<W: Write> QuadSerializer<W> for WriterNTriplesSerializer<W> {
735    fn serialize_quad(&mut self, quad: crate::model::QuadRef<'_>) -> SerializeResult<()> {
736        // N-Triples only supports default graph, so ignore named graphs
737        if quad.graph_name().is_default_graph() {
738            self.serialize_triple(quad.triple())
739        } else {
740            // Could log a warning here about ignoring named graph
741            Ok(())
742        }
743    }
744
745    fn finish(self: Box<Self>) -> SerializeResult<W> {
746        Ok(self.writer)
747    }
748}
749
750#[cfg(test)]
751mod tests {
752    use super::*;
753
754    #[test]
755    fn test_ntriples_parser_creation() {
756        let parser = NTriplesParser::new();
757        assert!(!parser.is_lenient());
758    }
759
760    #[test]
761    fn test_ntriples_parser_lenient() {
762        let parser = NTriplesParser::new().lenient();
763        assert!(parser.is_lenient());
764    }
765
766    #[test]
767    fn test_ntriples_parser_lenient_skips_invalid_line_via_tracing() {
768        // Regression test: lenient mode must skip invalid lines and keep
769        // parsing valid ones, routing the diagnostic through `tracing`
770        // rather than `eprintln!` (verified structurally here; the actual
771        // log routing is exercised by the tracing macro at the call site).
772        let input = "<http://example.org/s> <http://example.org/p> \"valid\" .\n\
773                      this is not a valid ntriples line\n\
774                      <http://example.org/s2> <http://example.org/p2> \"valid2\" .\n";
775        let parser = NTriplesParser::new().lenient();
776        let triples = parser.parse_str(input).expect("lenient parse succeeds");
777        assert_eq!(triples.len(), 2);
778
779        // Strict mode must still surface the same error instead of
780        // silently skipping it.
781        let strict_parser = NTriplesParser::new();
782        assert!(strict_parser.parse_str(input).is_err());
783    }
784
785    #[test]
786    fn test_ntriples_serializer_creation() {
787        let serializer = NTriplesSerializer::new();
788        assert!(serializer.is_validating());
789    }
790
791    #[test]
792    fn test_ntriples_serializer_unvalidated() {
793        let serializer = NTriplesSerializer::new().unvalidated();
794        assert!(!serializer.is_validating());
795    }
796
797    #[test]
798    fn test_empty_ntriples_parsing() {
799        let parser = NTriplesParser::new();
800        let result = parser.parse_str("");
801        assert!(result.is_ok());
802        assert!(result.expect("should have value").is_empty());
803    }
804
805    #[test]
806    fn test_ntriples_comments() {
807        let parser = NTriplesParser::new();
808        let ntriples = "# This is a comment\n# Another comment";
809        let result = parser.parse_str(ntriples);
810        assert!(result.is_ok());
811        assert!(result.expect("should have value").is_empty());
812    }
813
814    #[test]
815    fn test_ntriples_line_validation() {
816        let parser = NTriplesParser::new();
817
818        // Missing dot should fail
819        let result = parser.parse_triple_line(
820            "<http://example.org/s> <http://example.org/p> <http://example.org/o>",
821            1,
822        );
823        assert!(result.is_err());
824
825        // Too few components should fail
826        let result = parser.parse_triple_line("<http://example.org/s> <http://example.org/p> .", 1);
827        assert!(result.is_err());
828    }
829
830    #[test]
831    fn test_ntriples_parsing() {
832        let parser = NTriplesParser::new();
833
834        // Test simple triple
835        let ntriples = "<http://example.org/s> <http://example.org/p> <http://example.org/o> .";
836        let result = parser.parse_str(ntriples);
837        assert!(result.is_ok());
838        let triples = result.expect("should have value");
839        assert_eq!(triples.len(), 1);
840
841        // Test with blank node
842        let ntriples = "_:s <http://example.org/p> \"literal\" .";
843        let result = parser.parse_str(ntriples);
844        assert!(result.is_ok());
845        let triples = result.expect("should have value");
846        assert_eq!(triples.len(), 1);
847
848        // Test with language literal
849        let ntriples = "<http://example.org/s> <http://example.org/p> \"hello\"@en .";
850        let result = parser.parse_str(ntriples);
851        assert!(result.is_ok());
852        let triples = result.expect("should have value");
853        assert_eq!(triples.len(), 1);
854
855        // Test with typed literal
856        let ntriples = "<http://example.org/s> <http://example.org/p> \"42\"^^<http://www.w3.org/2001/XMLSchema#integer> .";
857        let result = parser.parse_str(ntriples);
858        assert!(result.is_ok());
859        let triples = result.expect("should have value");
860        assert_eq!(triples.len(), 1);
861    }
862
863    #[test]
864    fn test_ntriples_serialization() {
865        let serializer = NTriplesSerializer::new();
866
867        // Create a simple triple
868        let subject = NamedNode::new("http://example.org/s").expect("valid IRI");
869        let predicate = NamedNode::new("http://example.org/p").expect("valid IRI");
870        let object = Literal::new("test");
871        let triple = Triple::new(subject, predicate, object);
872
873        let result = serializer.serialize_to_string(&[triple]);
874        assert!(result.is_ok());
875        let output = result.expect("should have value");
876        assert!(output.contains("<http://example.org/s>"));
877        assert!(output.contains("<http://example.org/p>"));
878        assert!(output.contains("\"test\""));
879        assert!(output.ends_with(" .\n"));
880    }
881
882    #[test]
883    fn test_unicode_escape_parsing() {
884        let parser = NTriplesParser::new();
885
886        // Test \uHHHH escape sequence (Euro symbol)
887        let ntriples = r#"<http://example.org/s> <http://example.org/p> "Euro: \u20AC" ."#;
888        let result = parser.parse_str(ntriples);
889        assert!(result.is_ok());
890        let triples = result.expect("should have value");
891        assert_eq!(triples.len(), 1);
892        if let crate::model::term::Object::Literal(lit) = triples[0].object() {
893            assert_eq!(lit.value(), "Euro: €");
894        } else {
895            panic!("Expected literal object");
896        }
897
898        // Test \UHHHHHHHH escape sequence (Emoji)
899        let ntriples = r#"<http://example.org/s> <http://example.org/p> "Smile: \U0001F600" ."#;
900        let result = parser.parse_str(ntriples);
901        assert!(result.is_ok());
902        let triples = result.expect("should have value");
903        assert_eq!(triples.len(), 1);
904        if let crate::model::term::Object::Literal(lit) = triples[0].object() {
905            assert_eq!(lit.value(), "Smile: πŸ˜€");
906        } else {
907            panic!("Expected literal object");
908        }
909    }
910
911    #[test]
912    fn test_escape_sequence_parsing() {
913        let parser = NTriplesParser::new();
914
915        // Test all basic escape sequences
916        let ntriples = r#"<http://example.org/s> <http://example.org/p> "Line 1\nLine 2\tTabbed\rCarriage Return\\Backslash\"Quote" ."#;
917        let result = parser.parse_str(ntriples);
918        assert!(result.is_ok());
919        let triples = result.expect("should have value");
920        assert_eq!(triples.len(), 1);
921        if let crate::model::term::Object::Literal(lit) = triples[0].object() {
922            assert_eq!(
923                lit.value(),
924                "Line 1\nLine 2\tTabbed\rCarriage Return\\Backslash\"Quote"
925            );
926        } else {
927            panic!("Expected literal object");
928        }
929    }
930
931    #[test]
932    fn test_unicode_escape_serialization() {
933        let serializer = NTriplesSerializer::new();
934
935        // Create a triple with Unicode characters
936        let subject = NamedNode::new("http://example.org/s").expect("valid IRI");
937        let predicate = NamedNode::new("http://example.org/p").expect("valid IRI");
938        let object = Literal::new("Hello δΈ–η•Œ 🌍");
939        let triple = Triple::new(subject, predicate, object);
940
941        let result = serializer.serialize_to_string(&[triple]);
942        assert!(result.is_ok());
943        let output = result.expect("should have value");
944
945        // Should contain Unicode escape sequences for non-ASCII characters
946        assert!(output.contains("\\u4E16")); // δΈ–
947        assert!(output.contains("\\u754C")); // η•Œ
948        assert!(output.contains("\\U0001F30D")); // 🌍
949    }
950
951    #[test]
952    fn test_invalid_unicode_escapes() {
953        let parser = NTriplesParser::new();
954
955        // Test invalid \u sequence (too few digits)
956        let ntriples = r#"<http://example.org/s> <http://example.org/p> "Invalid: \u123" ."#;
957        let result = parser.parse_str(ntriples);
958        assert!(result.is_err());
959
960        // Test invalid \U sequence (too few digits)
961        let ntriples = r#"<http://example.org/s> <http://example.org/p> "Invalid: \U1234567" ."#;
962        let result = parser.parse_str(ntriples);
963        assert!(result.is_err());
964
965        // Test invalid hex digits
966        let ntriples = r#"<http://example.org/s> <http://example.org/p> "Invalid: \uGHIJ" ."#;
967        let result = parser.parse_str(ntriples);
968        assert!(result.is_err());
969    }
970}