Skip to main content

oxirs_core/parser/
mod.rs

1//! RDF parsing utilities for various formats with high-performance streaming
2//!
3//! **Stability**: ✅ **Stable** - Core parser APIs are production-ready.
4//!
5//! This module provides parsers for all major RDF serialization formats:
6//! - **Turtle** (.ttl) - A compact, human-readable format
7//! - **N-Triples** (.nt) - Line-based triple format
8//! - **TriG** (.trig) - Turtle with named graphs
9//! - **N-Quads** (.nq) - Line-based quad format
10//! - **RDF/XML** (.rdf, .xml) - XML-based format
11//! - **JSON-LD** (.jsonld) - JSON-based linked data format
12//!
13//! ## Features
14//!
15//! - **Streaming parsers** - Process large files without loading into memory
16//! - **Error recovery** - Continue parsing after encountering errors (optional)
17//! - **Base IRI resolution** - Resolve relative IRIs against a base
18//! - **Format detection** - Automatic format detection from file extensions or content
19//! - **Async support** - Non-blocking I/O for high-throughput applications
20//!
21//! ## Examples
22//!
23//! ### Basic Parsing
24//!
25//! ```rust
26//! use oxirs_core::parser::{Parser, RdfFormat};
27//!
28//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
29//! let turtle_data = r#"
30//!     @prefix foaf: <http://xmlns.com/foaf/0.1/> .
31//!
32//!     <http://example.org/alice> foaf:name "Alice" ;
33//!                                 foaf:knows <http://example.org/bob> .
34//! "#;
35//!
36//! let parser = Parser::new(RdfFormat::Turtle);
37//! let quads = parser.parse_str_to_quads(turtle_data)?;
38//!
39//! println!("Parsed {} quads", quads.len());
40//! # Ok(())
41//! # }
42//! ```
43//!
44//! ### Parsing with Configuration
45//!
46//! ```rust,ignore
47//! use oxirs_core::parser::{Parser, RdfFormat, ParserConfig};
48//!
49//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
50//! let config = ParserConfig {
51//!     base_iri: Some("http://example.org/base/".to_string()),
52//!     ignore_errors: true,
53//!     max_errors: Some(10),
54//! };
55//!
56//! let parser = Parser::new(RdfFormat::Turtle).with_config(config);
57//! let quads = parser.parse_str_to_quads("<relative> <p> <o> .")?;
58//! # Ok(())
59//! # }
60//! ```
61//!
62//! ### Format Detection
63//!
64//! ```rust,ignore
65//! use oxirs_core::parser::RdfFormat;
66//!
67//! // Detect from file extension
68//! let format = RdfFormat::from_extension("ttl");
69//! assert_eq!(format, Some(RdfFormat::Turtle));
70//!
71//! // Check format capabilities
72//! assert!(!RdfFormat::Turtle.supports_quads());
73//! assert!(RdfFormat::TriG.supports_quads());
74//! ```
75//!
76//! ### Streaming Large Files
77//!
78//! The high-level [`Parser`] in this module only exposes in-memory
79//! string/byte entry points ([`Parser::parse_str_to_quads`],
80//! [`Parser::parse_bytes_to_quads`], [`Parser::parse_str_with_handler`]) and
81//! always materializes the whole document before parsing, for every format.
82//! For genuine bounded-memory streaming from a [`std::io::Read`] source, use
83//! the lower-level [`crate::format::RdfParser`] instead, which reads
84//! incrementally for all six supported formats:
85//!
86//! ```rust,no_run
87//! use oxirs_core::format::{RdfParser, RdfFormat};
88//! use std::fs::File;
89//! use std::io::BufReader;
90//!
91//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
92//! let file = File::open("large_dataset.nt")?;
93//! let reader = BufReader::new(file);
94//!
95//! let parser = RdfParser::new(RdfFormat::NTriples);
96//! for quad in parser.for_reader(reader) {
97//!     let quad = quad?;
98//!     // Process quad without loading the entire file into memory
99//! }
100//! # Ok(())
101//! # }
102//! ```
103//!
104//! ### Async Parsing (with `async` feature)
105//!
106//! ```rust,no_run
107//! # #[cfg(feature = "async")]
108//! use oxirs_core::parser::{AsyncStreamingParser, RdfFormat};
109//!
110//! # #[cfg(feature = "async")]
111//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
112//! let parser = AsyncStreamingParser::new(RdfFormat::Turtle);
113//! let mut quads = Vec::new();
114//! parser.parse_stream(tokio::io::stdin(), |quad| {
115//!     quads.push(quad);
116//!     async { Ok(()) }
117//! }).await?;
118//! # Ok(())
119//! # }
120//! ```
121//!
122//! ## Performance Tips
123//!
124//! 1. **Use streaming** - For large files, use [`crate::format::RdfParser::for_reader`]
125//!    (not the [`Parser`] in this module, which always buffers the whole document)
126//!    to avoid loading everything into memory
127//! 2. **Choose the right format** - N-Triples/N-Quads are fastest to parse (line-based)
128//! 3. **Enable async** - [`AsyncStreamingParser`] truly streams N-Triples/N-Quads
129//!    incrementally; other formats are buffered up to a configurable limit
130//!    (see [`AsyncStreamingParser::with_max_buffer_size`]) before parsing
131//! 4. **Batch processing** - Process multiple files in parallel using rayon
132//!
133//! ## Error Handling
134//!
135//! Parsers can be configured to handle errors in different ways:
136//!
137//! - **Strict mode** (default) - Stop on first error
138//! - **Error recovery** - Collect errors and continue parsing
139//! - **Max errors** - Stop after a threshold of errors
140//!
141//! ## Format Support Matrix
142//!
143//! "Streaming" below means [`crate::format::RdfParser::for_reader`] (bounded
144//! memory, reads incrementally). The [`Parser`] in *this* module always
145//! buffers the whole input first regardless of format; [`AsyncStreamingParser`]
146//! truly streams only N-Triples/N-Quads (see the Performance Tips above).
147//!
148//! | Format | Triples | Quads | Prefixes | Comments | Streaming |
149//! |--------|---------|-------|----------|----------|-----------|
150//! | Turtle | ✅ | ❌ | ✅ | ✅ | ✅ |
151//! | N-Triples | ✅ | ❌ | ❌ | ✅ | ✅ |
152//! | TriG | ✅ | ✅ | ✅ | ✅ | ✅ |
153//! | N-Quads | ✅ | ✅ | ❌ | ✅ | ✅ |
154//! | RDF/XML | ✅ | ❌ | ✅ | ✅ | ✅ |
155//! | JSON-LD | ✅ | ✅ | ✅ | ❌ | ✅ |
156//!
157//! ## Related Modules
158//!
159//! - [`crate::serializer`] - Serialize RDF to various formats
160//! - [`crate::model`] - RDF data model types
161//! - [`crate::rdf_store`] - Store parsed RDF data
162
163#[cfg(feature = "async")]
164mod async_parser;
165
166#[cfg(feature = "async")]
167pub use async_parser::{AsyncRdfSink, AsyncStreamingParser, MemoryAsyncSink, ParseProgress};
168
169// Native implementation - no external dependencies needed
170use crate::model::{
171    BlankNode, GraphName, Literal, NamedNode, Object, Predicate, Quad, Subject, Triple,
172};
173use crate::{OxirsError, Result};
174
175/// RDF format enumeration
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
177pub enum RdfFormat {
178    /// Turtle format (TTL)
179    Turtle,
180    /// N-Triples format (NT)
181    NTriples,
182    /// TriG format (named graphs)
183    TriG,
184    /// N-Quads format
185    NQuads,
186    /// RDF/XML format
187    RdfXml,
188    /// JSON-LD format
189    JsonLd,
190}
191
192impl RdfFormat {
193    /// Detect format from file extension
194    pub fn from_extension(ext: &str) -> Option<Self> {
195        match ext.to_lowercase().as_str() {
196            "ttl" | "turtle" => Some(RdfFormat::Turtle),
197            "nt" | "ntriples" => Some(RdfFormat::NTriples),
198            "trig" => Some(RdfFormat::TriG),
199            "nq" | "nquads" => Some(RdfFormat::NQuads),
200            "rdf" | "xml" | "rdfxml" => Some(RdfFormat::RdfXml),
201            "jsonld" | "json-ld" => Some(RdfFormat::JsonLd),
202            _ => None,
203        }
204    }
205
206    /// Get the media type for this format
207    pub fn media_type(&self) -> &'static str {
208        match self {
209            RdfFormat::Turtle => "text/turtle",
210            RdfFormat::NTriples => "application/n-triples",
211            RdfFormat::TriG => "application/trig",
212            RdfFormat::NQuads => "application/n-quads",
213            RdfFormat::RdfXml => "application/rdf+xml",
214            RdfFormat::JsonLd => "application/ld+json",
215        }
216    }
217
218    /// Get file extension for this format
219    pub fn extension(&self) -> &'static str {
220        match self {
221            RdfFormat::Turtle => "ttl",
222            RdfFormat::NTriples => "nt",
223            RdfFormat::TriG => "trig",
224            RdfFormat::NQuads => "nq",
225            RdfFormat::RdfXml => "rdf",
226            RdfFormat::JsonLd => "jsonld",
227        }
228    }
229
230    /// Returns true if this format supports named graphs (quads)
231    pub fn supports_quads(&self) -> bool {
232        matches!(self, RdfFormat::TriG | RdfFormat::NQuads)
233    }
234}
235
236/// Configuration for RDF parsing
237#[derive(Debug, Clone, Default)]
238pub struct ParserConfig {
239    /// Base IRI for resolving relative IRIs
240    pub base_iri: Option<String>,
241    /// Whether to ignore parsing errors and continue
242    pub ignore_errors: bool,
243    /// Maximum number of errors to collect before stopping
244    pub max_errors: Option<usize>,
245}
246
247/// RDF parser interface
248#[derive(Debug, Clone)]
249pub struct Parser {
250    format: RdfFormat,
251    config: ParserConfig,
252}
253
254impl Parser {
255    /// Create a new parser for the specified format
256    pub fn new(format: RdfFormat) -> Self {
257        Parser {
258            format,
259            config: ParserConfig::default(),
260        }
261    }
262
263    /// Create a parser with custom configuration
264    pub fn with_config(format: RdfFormat, config: ParserConfig) -> Self {
265        Parser { format, config }
266    }
267
268    /// Set the base IRI for resolving relative IRIs
269    pub fn with_base_iri(mut self, base_iri: impl Into<String>) -> Self {
270        self.config.base_iri = Some(base_iri.into());
271        self
272    }
273
274    /// Enable or disable error tolerance
275    pub fn with_error_tolerance(mut self, ignore_errors: bool) -> Self {
276        self.config.ignore_errors = ignore_errors;
277        self
278    }
279
280    /// Parse RDF data from a string into a vector of quads
281    pub fn parse_str_to_quads(&self, data: &str) -> Result<Vec<Quad>> {
282        let mut quads = Vec::new();
283        self.parse_str_with_handler(data, |quad| {
284            quads.push(quad);
285            Ok(())
286        })?;
287        Ok(quads)
288    }
289
290    /// Parse RDF data from a string into a vector of triples (only default graph)
291    pub fn parse_str_to_triples(&self, data: &str) -> Result<Vec<Triple>> {
292        let quads = self.parse_str_to_quads(data)?;
293        Ok(quads
294            .into_iter()
295            .filter(|quad| quad.is_default_graph())
296            .map(|quad| quad.to_triple())
297            .collect())
298    }
299
300    /// Parse RDF data with a custom handler for each quad
301    pub fn parse_str_with_handler<F>(&self, data: &str, handler: F) -> Result<()>
302    where
303        F: FnMut(Quad) -> Result<()>,
304    {
305        match self.format {
306            RdfFormat::Turtle => self.parse_turtle(data, handler),
307            RdfFormat::NTriples => self.parse_ntriples(data, handler),
308            RdfFormat::TriG => self.parse_trig(data, handler),
309            RdfFormat::NQuads => self.parse_nquads(data, handler),
310            RdfFormat::RdfXml => self.parse_rdfxml(data, handler),
311            RdfFormat::JsonLd => self.parse_jsonld(data, handler),
312        }
313    }
314
315    /// Parse RDF data from bytes
316    pub fn parse_bytes_to_quads(&self, data: &[u8]) -> Result<Vec<Quad>> {
317        let data_str = std::str::from_utf8(data)
318            .map_err(|e| OxirsError::Parse(format!("Invalid UTF-8: {e}")))?;
319        self.parse_str_to_quads(data_str)
320    }
321
322    fn parse_turtle<F>(&self, data: &str, mut handler: F) -> Result<()>
323    where
324        F: FnMut(Quad) -> Result<()>,
325    {
326        // Delegate to the real, oxttl-backed grammar (crate::format::turtle
327        // via crate::format::RdfParser) instead of a hand-rolled line-based
328        // state machine. This correctly handles semicolons/commas inside
329        // quoted literals, comma object lists, collections `( ... )`,
330        // blank-node property lists `[ ... ]`, and multi-line triple-quoted
331        // string literals -- none of which a per-line splitter can support.
332        let mut internal_parser = crate::format::RdfParser::new(crate::format::RdfFormat::Turtle);
333        if let Some(base) = &self.config.base_iri {
334            internal_parser = internal_parser.with_base_iri(base.clone());
335        }
336
337        for result in internal_parser.for_slice(data.as_bytes()) {
338            match result {
339                Ok(quad) => handler(quad)?,
340                Err(e) => {
341                    if self.config.ignore_errors {
342                        tracing::warn!("Turtle parse error: {e}");
343                        continue;
344                    } else {
345                        return Err(OxirsError::Parse(format!("Turtle parse error: {e}")));
346                    }
347                }
348            }
349        }
350
351        Ok(())
352    }
353
354    fn parse_ntriples<F>(&self, data: &str, mut handler: F) -> Result<()>
355    where
356        F: FnMut(Quad) -> Result<()>,
357    {
358        for (line_num, line) in data.lines().enumerate() {
359            let line = line.trim();
360
361            // Skip empty lines and comments
362            if line.is_empty() || line.starts_with('#') {
363                continue;
364            }
365
366            // Parse the line into a triple
367            match self.parse_ntriples_line(line) {
368                Ok(Some(quad)) => {
369                    handler(quad)?;
370                }
371                Ok(None) => {
372                    // Skip this line (e.g., blank line)
373                    continue;
374                }
375                Err(e) => {
376                    if self.config.ignore_errors {
377                        tracing::warn!("Parse error on line {}: {}", line_num + 1, e);
378                        continue;
379                    } else {
380                        return Err(OxirsError::Parse(format!(
381                            "Parse error on line {}: {}",
382                            line_num + 1,
383                            e
384                        )));
385                    }
386                }
387            }
388        }
389
390        Ok(())
391    }
392
393    pub fn parse_ntriples_line(&self, line: &str) -> Result<Option<Quad>> {
394        // Simple N-Triples parser - parse line like: <s> <p> "o" .
395        let line = line.trim();
396
397        if line.is_empty() || line.starts_with('#') {
398            return Ok(None);
399        }
400
401        // Find the final period
402        if !line.ends_with('.') {
403            return Err(OxirsError::Parse("Line must end with '.'".to_string()));
404        }
405
406        let line = &line[..line.len() - 1].trim(); // Remove trailing period and whitespace
407
408        // Split into tokens respecting quoted strings
409        let tokens = self.tokenize_ntriples_line(line)?;
410
411        if tokens.len() != 3 {
412            return Err(OxirsError::Parse(format!(
413                "Expected 3 tokens (subject, predicate, object), found {}",
414                tokens.len()
415            )));
416        }
417
418        // Parse subject
419        let subject = self.parse_subject(&tokens[0])?;
420
421        // Parse predicate
422        let predicate = self.parse_predicate(&tokens[1])?;
423
424        // Parse object
425        let object = self.parse_object(&tokens[2])?;
426
427        let triple = Triple::new(subject, predicate, object);
428        let quad = Quad::from_triple(triple);
429
430        Ok(Some(quad))
431    }
432
433    fn tokenize_ntriples_line(&self, line: &str) -> Result<Vec<String>> {
434        let mut tokens = Vec::new();
435        let mut current_token = String::new();
436        let mut in_quotes = false;
437        let mut escaped = false;
438        let mut chars = line.chars().peekable();
439
440        while let Some(c) = chars.next() {
441            if escaped {
442                // Preserve escape sequences - don't unescape during tokenization
443                current_token.push('\\');
444                current_token.push(c);
445                escaped = false;
446            } else if c == '\\' && in_quotes {
447                escaped = true;
448            } else if c == '"' && !escaped {
449                current_token.push(c);
450                if in_quotes {
451                    // Check for language tag or datatype after closing quote
452                    if let Some(&'@') = chars.peek() {
453                        // Language tag
454                        current_token.push(chars.next().expect("peeked '@' should be available"));
455                        while let Some(&next_char) = chars.peek() {
456                            if next_char.is_alphanumeric() || next_char == '-' {
457                                current_token
458                                    .push(chars.next().expect("peeked char should be available"));
459                            } else {
460                                break;
461                            }
462                        }
463                    } else if chars.peek() == Some(&'^') {
464                        // Datatype
465                        chars.next(); // first ^
466                        if chars.peek() == Some(&'^') {
467                            chars.next(); // second ^
468                            current_token.push_str("^^");
469                            if chars.peek() == Some(&'<') {
470                                // IRI datatype
471                                for next_char in chars.by_ref() {
472                                    current_token.push(next_char);
473                                    if next_char == '>' {
474                                        break;
475                                    }
476                                }
477                            }
478                        }
479                    }
480                    in_quotes = false;
481                } else {
482                    in_quotes = true;
483                }
484            } else if c == '"' && escaped {
485                // This is an escaped quote, add it to the token
486                current_token.push(c);
487                escaped = false;
488            } else if c.is_whitespace() && !in_quotes {
489                if !current_token.is_empty() {
490                    tokens.push(current_token.clone());
491                    current_token.clear();
492                }
493            } else {
494                current_token.push(c);
495            }
496        }
497
498        if !current_token.is_empty() {
499            tokens.push(current_token);
500        }
501
502        Ok(tokens)
503    }
504
505    fn parse_subject(&self, token: &str) -> Result<Subject> {
506        if token.starts_with('<') && token.ends_with('>') {
507            let iri = &token[1..token.len() - 1];
508            let named_node = NamedNode::new(iri)?;
509            Ok(Subject::NamedNode(named_node))
510        } else if token.starts_with("_:") {
511            let blank_node = BlankNode::new(token)?;
512            Ok(Subject::BlankNode(blank_node))
513        } else {
514            Err(OxirsError::Parse(format!(
515                "Invalid subject: {token}. Must be IRI or blank node"
516            )))
517        }
518    }
519
520    fn parse_predicate(&self, token: &str) -> Result<Predicate> {
521        if token.starts_with('<') && token.ends_with('>') {
522            let iri = &token[1..token.len() - 1];
523            let named_node = NamedNode::new(iri)?;
524            Ok(Predicate::NamedNode(named_node))
525        } else {
526            Err(OxirsError::Parse(format!(
527                "Invalid predicate: {token}. Must be IRI"
528            )))
529        }
530    }
531
532    fn parse_object(&self, token: &str) -> Result<Object> {
533        if token.starts_with('<') && token.ends_with('>') {
534            // IRI
535            let iri = &token[1..token.len() - 1];
536            let named_node = NamedNode::new(iri)?;
537            Ok(Object::NamedNode(named_node))
538        } else if token.starts_with("_:") {
539            // Blank node
540            let blank_node = BlankNode::new(token)?;
541            Ok(Object::BlankNode(blank_node))
542        } else if token.starts_with('"') {
543            // Literal
544            self.parse_literal(token)
545        } else {
546            Err(OxirsError::Parse(format!(
547                "Invalid object: {token}. Must be IRI, blank node, or literal"
548            )))
549        }
550    }
551
552    fn parse_literal(&self, token: &str) -> Result<Object> {
553        if !token.starts_with('"') {
554            return Err(OxirsError::Parse(
555                "Literal must start with quote".to_string(),
556            ));
557        }
558
559        // Find the closing quote. We scan via `char_indices` so that
560        // `end_quote_pos` is a *byte* offset into `token` from the start —
561        // matching the byte-indexed slicing below. The previous
562        // implementation collected into `Vec<char>` and used the resulting
563        // *char* index to slice the original `&str`, which panics on any
564        // literal containing multi-byte UTF-8 characters (e.g. Japanese
565        // text, emoji, accented Latin) once the char index diverges from
566        // the byte offset.
567        let mut end_quote_pos = None;
568        let mut escaped = false;
569
570        for (i, ch) in token.char_indices().skip(1) {
571            if escaped {
572                escaped = false;
573                continue;
574            }
575
576            if ch == '\\' {
577                escaped = true;
578            } else if ch == '"' {
579                end_quote_pos = Some(i);
580                break;
581            }
582        }
583
584        let end_quote_pos =
585            end_quote_pos.ok_or_else(|| OxirsError::Parse("Unterminated literal".to_string()))?;
586
587        // Extract the literal value (without quotes) and unescape.
588        // `1` and `end_quote_pos` are both valid char-boundary byte offsets
589        // (the opening quote and closing quote are each a single ASCII
590        // byte), so this slice is safe even when the value itself contains
591        // multi-byte characters.
592        let raw_value = &token[1..end_quote_pos];
593        let literal_value = self.unescape_literal_value(raw_value)?;
594
595        // Check for language tag or datatype
596        let remaining = &token[end_quote_pos + 1..];
597
598        if let Some(lang_tag) = remaining.strip_prefix('@') {
599            // Language tag
600            let literal = Literal::new_lang(literal_value, lang_tag)?;
601            Ok(Object::Literal(literal))
602        } else if remaining.starts_with("^^<") && remaining.ends_with('>') {
603            // Datatype
604            let datatype_iri = &remaining[3..remaining.len() - 1];
605            let datatype = NamedNode::new(datatype_iri)?;
606            let literal = Literal::new_typed(literal_value, datatype);
607            Ok(Object::Literal(literal))
608        } else if remaining.is_empty() {
609            // Plain literal
610            let literal = Literal::new(literal_value);
611            Ok(Object::Literal(literal))
612        } else {
613            Err(OxirsError::Parse(format!(
614                "Invalid literal syntax: {token}"
615            )))
616        }
617    }
618
619    fn parse_trig<F>(&self, data: &str, mut handler: F) -> Result<()>
620    where
621        F: FnMut(Quad) -> Result<()>,
622    {
623        // Delegate to the real, oxttl-backed TriG grammar (same rationale as
624        // parse_turtle above: named-graph blocks, nested Turtle syntax, and
625        // multi-line statements are not safely handled by line splitting).
626        let mut internal_parser = crate::format::RdfParser::new(crate::format::RdfFormat::TriG);
627        if let Some(base) = &self.config.base_iri {
628            internal_parser = internal_parser.with_base_iri(base.clone());
629        }
630
631        for result in internal_parser.for_slice(data.as_bytes()) {
632            match result {
633                Ok(quad) => handler(quad)?,
634                Err(e) => {
635                    if self.config.ignore_errors {
636                        tracing::warn!("TriG parse error: {e}");
637                        continue;
638                    } else {
639                        return Err(OxirsError::Parse(format!("TriG parse error: {e}")));
640                    }
641                }
642            }
643        }
644
645        Ok(())
646    }
647
648    fn parse_nquads<F>(&self, data: &str, mut handler: F) -> Result<()>
649    where
650        F: FnMut(Quad) -> Result<()>,
651    {
652        for (line_num, line) in data.lines().enumerate() {
653            let line = line.trim();
654
655            // Skip empty lines and comments
656            if line.is_empty() || line.starts_with('#') {
657                continue;
658            }
659
660            // Parse the line into a quad
661            match self.parse_nquads_line(line) {
662                Ok(Some(quad)) => {
663                    handler(quad)?;
664                }
665                Ok(None) => {
666                    // Skip this line (e.g., blank line)
667                    continue;
668                }
669                Err(e) => {
670                    if self.config.ignore_errors {
671                        tracing::warn!("Parse error on line {}: {}", line_num + 1, e);
672                        continue;
673                    } else {
674                        return Err(OxirsError::Parse(format!(
675                            "Parse error on line {}: {}",
676                            line_num + 1,
677                            e
678                        )));
679                    }
680                }
681            }
682        }
683
684        Ok(())
685    }
686
687    pub fn parse_nquads_line(&self, line: &str) -> Result<Option<Quad>> {
688        // N-Quads parser - parse line like: <s> <p> "o" <g> .
689        let line = line.trim();
690
691        if line.is_empty() || line.starts_with('#') {
692            return Ok(None);
693        }
694
695        // Find the final period
696        if !line.ends_with('.') {
697            return Err(OxirsError::Parse("Line must end with '.'".to_string()));
698        }
699
700        let line = &line[..line.len() - 1].trim(); // Remove trailing period and whitespace
701
702        // Split into tokens respecting quoted strings
703        let tokens = self.tokenize_ntriples_line(line)?;
704
705        if tokens.len() != 4 {
706            return Err(OxirsError::Parse(format!(
707                "Expected 4 tokens (subject, predicate, object, graph), found {}",
708                tokens.len()
709            )));
710        }
711
712        // Parse subject
713        let subject = self.parse_subject(&tokens[0])?;
714
715        // Parse predicate
716        let predicate = self.parse_predicate(&tokens[1])?;
717
718        // Parse object
719        let object = self.parse_object(&tokens[2])?;
720
721        // Parse graph name
722        let graph_name = self.parse_graph_name(&tokens[3])?;
723
724        let quad = Quad::new(subject, predicate, object, graph_name);
725
726        Ok(Some(quad))
727    }
728
729    fn parse_graph_name(&self, token: &str) -> Result<GraphName> {
730        if token.starts_with('<') && token.ends_with('>') {
731            let iri = &token[1..token.len() - 1];
732            let named_node = NamedNode::new(iri)?;
733            Ok(GraphName::NamedNode(named_node))
734        } else if token.starts_with("_:") {
735            let blank_node = BlankNode::new(token)?;
736            Ok(GraphName::BlankNode(blank_node))
737        } else {
738            Err(OxirsError::Parse(format!(
739                "Invalid graph name: {token}. Must be IRI or blank node"
740            )))
741        }
742    }
743
744    fn parse_rdfxml<F>(&self, data: &str, mut handler: F) -> Result<()>
745    where
746        F: FnMut(Quad) -> Result<()>,
747    {
748        use crate::rdfxml::wrapper::parse_rdfxml;
749        use std::io::Cursor;
750
751        // Parse RDF/XML data using the wrapper
752        let reader = Cursor::new(data.as_bytes());
753        let base_iri = self.config.base_iri.as_deref();
754        let quads = parse_rdfxml(reader, base_iri, self.config.ignore_errors)?;
755
756        // Process each quad through the handler
757        for quad in quads {
758            handler(quad)?;
759        }
760
761        Ok(())
762    }
763
764    fn parse_jsonld<F>(&self, data: &str, mut handler: F) -> Result<()>
765    where
766        F: FnMut(Quad) -> Result<()>,
767    {
768        // Basic JSON-LD parser implementation using existing jsonld module
769        use crate::jsonld::to_rdf::JsonLdParser;
770
771        let parser = JsonLdParser::new();
772        let parser = if let Some(base_iri) = &self.config.base_iri {
773            parser
774                .with_base_iri(base_iri.clone())
775                .map_err(|e| OxirsError::Parse(format!("Invalid base IRI: {e}")))?
776        } else {
777            parser
778        };
779
780        // Parse JSON-LD data into quads
781        for result in parser.for_slice(data.as_bytes()) {
782            match result {
783                Ok(quad) => handler(quad)?,
784                Err(e) => {
785                    if self.config.ignore_errors {
786                        tracing::warn!("JSON-LD parse error: {}", e);
787                        continue;
788                    } else {
789                        return Err(OxirsError::Parse(format!("JSON-LD parse error: {e}")));
790                    }
791                }
792            }
793        }
794
795        Ok(())
796    }
797
798    /// Unescape special characters in literal values
799    fn unescape_literal_value(&self, value: &str) -> Result<String> {
800        let mut result = String::new();
801        let mut chars = value.chars();
802
803        while let Some(c) = chars.next() {
804            if c == '\\' {
805                match chars.next() {
806                    Some('"') => result.push('"'),
807                    Some('\\') => result.push('\\'),
808                    Some('n') => result.push('\n'),
809                    Some('r') => result.push('\r'),
810                    Some('t') => result.push('\t'),
811                    Some('u') => {
812                        // Parse \uHHHH Unicode escape
813                        let hex_chars: String = chars.by_ref().take(4).collect();
814                        if hex_chars.len() != 4 {
815                            return Err(OxirsError::Parse(
816                                "Invalid Unicode escape sequence \\uHHHH - expected 4 hex digits"
817                                    .to_string(),
818                            ));
819                        }
820                        let code_point = u32::from_str_radix(&hex_chars, 16).map_err(|_| {
821                            OxirsError::Parse(
822                                "Invalid hex digits in Unicode escape sequence".to_string(),
823                            )
824                        })?;
825                        let unicode_char = char::from_u32(code_point).ok_or_else(|| {
826                            OxirsError::Parse("Invalid Unicode code point".to_string())
827                        })?;
828                        result.push(unicode_char);
829                    }
830                    Some('U') => {
831                        // Parse \UHHHHHHHH Unicode escape
832                        let hex_chars: String = chars.by_ref().take(8).collect();
833                        if hex_chars.len() != 8 {
834                            return Err(OxirsError::Parse(
835                                "Invalid Unicode escape sequence \\UHHHHHHHH - expected 8 hex digits".to_string()
836                            ));
837                        }
838                        let code_point = u32::from_str_radix(&hex_chars, 16).map_err(|_| {
839                            OxirsError::Parse(
840                                "Invalid hex digits in Unicode escape sequence".to_string(),
841                            )
842                        })?;
843                        let unicode_char = char::from_u32(code_point).ok_or_else(|| {
844                            OxirsError::Parse("Invalid Unicode code point".to_string())
845                        })?;
846                        result.push(unicode_char);
847                    }
848                    Some(other) => {
849                        return Err(OxirsError::Parse(format!(
850                            "Invalid escape sequence \\{other}"
851                        )));
852                    }
853                    None => {
854                        return Err(OxirsError::Parse(
855                            "Incomplete escape sequence at end of literal".to_string(),
856                        ));
857                    }
858                }
859            } else {
860                result.push(c);
861            }
862        }
863
864        Ok(result)
865    }
866
867    // Native parsing implementation complete - no external dependencies needed
868}
869
870/// Convenience function to detect RDF format from content
871pub fn detect_format_from_content(content: &str) -> Option<RdfFormat> {
872    let content = content.trim();
873
874    // Check for XML-like content (RDF/XML)
875    if content.starts_with("<?xml")
876        || content.starts_with("<rdf:RDF")
877        || content.starts_with("<RDF")
878    {
879        return Some(RdfFormat::RdfXml);
880    }
881
882    // Check for JSON-LD
883    if content.starts_with('{') && (content.contains("@context") || content.contains("@type")) {
884        return Some(RdfFormat::JsonLd);
885    }
886
887    // Check for Turtle syntax elements first (has priority over N-Quads/N-Triples)
888    if content.contains("@prefix") || content.contains("@base") || content.contains(';') {
889        return Some(RdfFormat::Turtle);
890    }
891
892    // Check for TriG (named graphs syntax)
893    if content.contains('{') && content.contains('}') {
894        return Some(RdfFormat::TriG);
895    }
896
897    // Count tokens in first meaningful line to distinguish N-Quads vs N-Triples
898    for line in content.lines() {
899        let line = line.trim();
900        if !line.is_empty() && !line.starts_with('#') {
901            let parts: Vec<&str> = line.split_whitespace().collect();
902            if parts.len() == 4 && parts[3] == "." {
903                // Exactly 4 parts (s p o .) - N-Triples
904                return Some(RdfFormat::NTriples);
905            } else if parts.len() == 5 && parts[4] == "." {
906                // Exactly 5 parts (s p o g .) - N-Quads
907                return Some(RdfFormat::NQuads);
908            } else if parts.len() >= 3 && parts[parts.len() - 1] == "." {
909                // Fallback: assume N-Triples for basic triple pattern
910                return Some(RdfFormat::NTriples);
911            }
912            break; // Only check first meaningful line
913        }
914    }
915
916    None
917}
918
919#[cfg(test)]
920mod tests {
921    use super::*;
922    use crate::model::graph::Graph;
923
924    #[test]
925    fn test_format_detection_from_extension() {
926        assert_eq!(RdfFormat::from_extension("ttl"), Some(RdfFormat::Turtle));
927        assert_eq!(RdfFormat::from_extension("turtle"), Some(RdfFormat::Turtle));
928        assert_eq!(RdfFormat::from_extension("nt"), Some(RdfFormat::NTriples));
929        assert_eq!(
930            RdfFormat::from_extension("ntriples"),
931            Some(RdfFormat::NTriples)
932        );
933        assert_eq!(RdfFormat::from_extension("trig"), Some(RdfFormat::TriG));
934        assert_eq!(RdfFormat::from_extension("nq"), Some(RdfFormat::NQuads));
935        assert_eq!(RdfFormat::from_extension("rdf"), Some(RdfFormat::RdfXml));
936        assert_eq!(RdfFormat::from_extension("jsonld"), Some(RdfFormat::JsonLd));
937        assert_eq!(RdfFormat::from_extension("unknown"), None);
938    }
939
940    #[test]
941    fn test_format_properties() {
942        assert_eq!(RdfFormat::Turtle.media_type(), "text/turtle");
943        assert_eq!(RdfFormat::NTriples.extension(), "nt");
944        assert!(RdfFormat::TriG.supports_quads());
945        assert!(!RdfFormat::Turtle.supports_quads());
946    }
947
948    #[test]
949    fn test_format_detection_from_content() {
950        // XML content
951        let xml_content = "<?xml version=\"1.0\"?>\n<rdf:RDF>";
952        assert_eq!(
953            detect_format_from_content(xml_content),
954            Some(RdfFormat::RdfXml)
955        );
956
957        // JSON-LD content
958        let jsonld_content = r#"{"@context": "http://example.org", "@type": "Person"}"#;
959        assert_eq!(
960            detect_format_from_content(jsonld_content),
961            Some(RdfFormat::JsonLd)
962        );
963
964        // Turtle content
965        let turtle_content = "@prefix foaf: <http://xmlns.com/foaf/0.1/> .";
966        assert_eq!(
967            detect_format_from_content(turtle_content),
968            Some(RdfFormat::Turtle)
969        );
970
971        // N-Triples content
972        let ntriples_content = "<http://example.org/s> <http://example.org/p> \"object\" .";
973        assert_eq!(
974            detect_format_from_content(ntriples_content),
975            Some(RdfFormat::NTriples)
976        );
977    }
978
979    #[test]
980    fn test_ntriples_parsing_simple() {
981        let ntriples_data = r#"<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" .
982<http://example.org/alice> <http://xmlns.com/foaf/0.1/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
983_:person1 <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> ."#;
984
985        let parser = Parser::new(RdfFormat::NTriples);
986        let result = parser.parse_str_to_quads(ntriples_data);
987
988        assert!(result.is_ok());
989        let quads = result.expect("should have value");
990        assert_eq!(quads.len(), 3);
991
992        // Check that all quads are in the default graph
993        for quad in &quads {
994            assert!(quad.is_default_graph());
995        }
996
997        // Convert to triples for easier checking
998        let triples: Vec<_> = quads.into_iter().map(|q| q.to_triple()).collect();
999
1000        // Check first triple
1001        let alice_iri = NamedNode::new("http://example.org/alice").expect("valid IRI");
1002        let name_pred = NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("valid IRI");
1003        let name_literal = Literal::new("Alice Smith");
1004        let expected_triple1 = Triple::new(alice_iri.clone(), name_pred, name_literal);
1005        assert!(triples.contains(&expected_triple1));
1006
1007        // Check typed literal triple
1008        let age_pred = NamedNode::new("http://xmlns.com/foaf/0.1/age").expect("valid IRI");
1009        let integer_type =
1010            NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").expect("valid IRI");
1011        let age_literal = Literal::new_typed("30", integer_type);
1012        let expected_triple2 = Triple::new(alice_iri, age_pred, age_literal);
1013        assert!(triples.contains(&expected_triple2));
1014
1015        // Check blank node triple
1016        let blank_node = BlankNode::new("_:person1").expect("valid blank node id");
1017        let knows_pred = NamedNode::new("http://xmlns.com/foaf/0.1/knows").expect("valid IRI");
1018        let bob_iri = NamedNode::new("http://example.org/bob").expect("valid IRI");
1019        let expected_triple3 = Triple::new(blank_node, knows_pred, bob_iri);
1020        assert!(triples.contains(&expected_triple3));
1021    }
1022
1023    #[test]
1024    fn test_ntriples_parsing_language_tag() {
1025        let ntriples_data =
1026            r#"<http://example.org/alice> <http://example.org/description> "Une personne"@fr ."#;
1027
1028        let parser = Parser::new(RdfFormat::NTriples);
1029        let result = parser.parse_str_to_quads(ntriples_data);
1030
1031        assert!(result.is_ok());
1032        let quads = result.expect("should have value");
1033        assert_eq!(quads.len(), 1);
1034
1035        let triple = quads[0].to_triple();
1036        if let Object::Literal(literal) = triple.object() {
1037            assert_eq!(literal.value(), "Une personne");
1038            assert_eq!(literal.language(), Some("fr"));
1039            assert!(literal.is_lang_string());
1040        } else {
1041            panic!("Expected literal object");
1042        }
1043    }
1044
1045    #[test]
1046    fn test_ntriples_parsing_escaped_literals() {
1047        let ntriples_data = r#"<http://example.org/test> <http://example.org/desc> "Text with \"quotes\" and \n newlines" ."#;
1048
1049        let parser = Parser::new(RdfFormat::NTriples);
1050        let result = parser.parse_str_to_quads(ntriples_data);
1051
1052        if let Err(e) = &result {
1053            println!("Parse error: {e}");
1054        }
1055        assert!(result.is_ok(), "Parse failed: {result:?}");
1056
1057        let quads = result.expect("should have value");
1058        assert_eq!(quads.len(), 1);
1059
1060        let triple = quads[0].to_triple();
1061        if let Object::Literal(literal) = triple.object() {
1062            assert!(literal.value().contains("\"quotes\""));
1063            assert!(literal.value().contains("\n"));
1064        } else {
1065            panic!("Expected literal object");
1066        }
1067    }
1068
1069    #[test]
1070    fn test_ntriples_parsing_comments_and_empty_lines() {
1071        let ntriples_data = r#"
1072# This is a comment
1073<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" .
1074
1075# Another comment
1076<http://example.org/bob> <http://xmlns.com/foaf/0.1/name> "Bob Jones" .
1077"#;
1078
1079        let parser = Parser::new(RdfFormat::NTriples);
1080        let result = parser.parse_str_to_quads(ntriples_data);
1081
1082        assert!(result.is_ok());
1083        let quads = result.expect("should have value");
1084        assert_eq!(quads.len(), 2);
1085    }
1086
1087    #[test]
1088    fn test_ntriples_parsing_error_handling() {
1089        // Test invalid syntax
1090        let invalid_data = "invalid ntriples data";
1091        let parser = Parser::new(RdfFormat::NTriples);
1092        let result = parser.parse_str_to_quads(invalid_data);
1093        assert!(result.is_err());
1094
1095        // Test error tolerance
1096        let mixed_data = r#"<http://example.org/valid> <http://example.org/pred> "Valid triple" .
1097invalid line here
1098<http://example.org/valid2> <http://example.org/pred> "Another valid triple" ."#;
1099
1100        let parser_strict = Parser::new(RdfFormat::NTriples);
1101        let result_strict = parser_strict.parse_str_to_quads(mixed_data);
1102        assert!(result_strict.is_err());
1103
1104        let parser_tolerant = Parser::new(RdfFormat::NTriples).with_error_tolerance(true);
1105        let result_tolerant = parser_tolerant.parse_str_to_quads(mixed_data);
1106        assert!(result_tolerant.is_ok());
1107        let quads = result_tolerant.expect("tolerant parse should succeed");
1108        assert_eq!(quads.len(), 2); // Should parse the two valid triples
1109    }
1110
1111    #[test]
1112    fn test_nquads_parsing() {
1113        let nquads_data = r#"<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" <http://example.org/graph1> .
1114<http://example.org/alice> <http://xmlns.com/foaf/0.1/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> <http://example.org/graph2> .
1115_:person1 <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> _:graph1 ."#;
1116
1117        let parser = Parser::new(RdfFormat::NQuads);
1118        let result = parser.parse_str_to_quads(nquads_data);
1119
1120        assert!(result.is_ok());
1121        let quads = result.expect("should have value");
1122        assert_eq!(quads.len(), 3);
1123
1124        // Check that quads have proper graph names
1125        let first_quad = &quads[0];
1126        assert!(!first_quad.is_default_graph());
1127
1128        // Check that we can extract graph names
1129        if let GraphName::NamedNode(graph_name) = first_quad.graph_name() {
1130            assert!(graph_name.as_str().contains("example.org"));
1131        } else {
1132            panic!("Expected named graph");
1133        }
1134    }
1135
1136    #[test]
1137    fn test_turtle_parsing_basic() {
1138        let turtle_data = r#"@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1139@prefix ex: <http://example.org/> .
1140
1141ex:alice foaf:name "Alice Smith" .
1142ex:alice foaf:age "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
1143ex:alice foaf:knows ex:bob ."#;
1144
1145        let parser = Parser::new(RdfFormat::Turtle);
1146        let result = parser.parse_str_to_quads(turtle_data);
1147
1148        assert!(result.is_ok());
1149        let quads = result.expect("should have value");
1150        assert_eq!(quads.len(), 3);
1151
1152        // All quads should be in default graph
1153        for quad in &quads {
1154            assert!(quad.is_default_graph());
1155        }
1156    }
1157
1158    #[test]
1159    fn test_turtle_parsing_prefixes() {
1160        let turtle_data = r#"@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1161foaf:Person a foaf:Person ."#;
1162
1163        let parser = Parser::new(RdfFormat::Turtle);
1164        let result = parser.parse_str_to_quads(turtle_data);
1165
1166        assert!(result.is_ok());
1167        let quads = result.expect("should have value");
1168        assert_eq!(quads.len(), 1);
1169
1170        let triple = quads[0].to_triple();
1171        // Should expand foaf:Person to full IRI
1172        if let Subject::NamedNode(subj) = triple.subject() {
1173            assert!(subj.as_str().contains("xmlns.com/foaf"));
1174        } else {
1175            panic!("Expected named node subject");
1176        }
1177
1178        // Predicate should be rdf:type (from 'a')
1179        if let Predicate::NamedNode(pred) = triple.predicate() {
1180            assert!(pred.as_str().contains("rdf-syntax-ns#type"));
1181        } else {
1182            panic!("Expected named node predicate");
1183        }
1184    }
1185
1186    #[test]
1187    fn test_turtle_parsing_abbreviated_syntax() {
1188        let turtle_data = r#"@prefix ex: <http://example.org/> .
1189@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1190
1191ex:alice foaf:name "Alice" ;
1192         foaf:age "30" ."#;
1193
1194        let parser = Parser::new(RdfFormat::Turtle);
1195        let result = parser.parse_str_to_quads(turtle_data);
1196
1197        assert!(result.is_ok());
1198        let quads = result.expect("should have value");
1199        assert_eq!(quads.len(), 2);
1200
1201        // Both triples should have the same subject
1202        let subjects: Vec<_> = quads
1203            .iter()
1204            .map(|q| q.to_triple().subject().clone())
1205            .collect();
1206        assert_eq!(subjects[0], subjects[1]);
1207    }
1208
1209    /// Regression test for the P0 finding: semicolon-splitting used to be
1210    /// done on the raw accumulated statement *before* any quote-aware
1211    /// tokenization, so a literal value containing a `;` corrupted the
1212    /// triple (spurious extra/garbled triples or parse errors). The real
1213    /// oxttl-backed grammar must treat `;` inside a string literal as plain
1214    /// literal content, not a predicate-object-list separator.
1215    #[test]
1216    fn test_turtle_semicolon_inside_literal_not_split() {
1217        let turtle_data = r#"@prefix ex: <http://example.org/> .
1218ex:alice ex:bio "Loves cats; dogs; and turtles" ;
1219         ex:name "Alice" ."#;
1220
1221        let parser = Parser::new(RdfFormat::Turtle);
1222        let quads = parser
1223            .parse_str_to_quads(turtle_data)
1224            .expect("semicolon inside a literal must not corrupt parsing");
1225
1226        assert_eq!(quads.len(), 2, "expected exactly 2 triples, got {quads:?}");
1227
1228        let bio_triple = quads
1229            .iter()
1230            .map(|q| q.to_triple())
1231            .find(|t| t.predicate().to_string().contains("bio"))
1232            .expect("bio triple should be present");
1233        if let Object::Literal(lit) = bio_triple.object() {
1234            assert_eq!(lit.value(), "Loves cats; dogs; and turtles");
1235        } else {
1236            panic!("Expected literal object for ex:bio");
1237        }
1238    }
1239
1240    /// Regression test for the P0 finding: the hand-rolled state machine had
1241    /// no support for comma-separated object lists (`predicate obj1, obj2`).
1242    #[test]
1243    fn test_turtle_comma_object_list() {
1244        let turtle_data = r#"@prefix ex: <http://example.org/> .
1245ex:alice ex:knows ex:bob, ex:carol, ex:dave ."#;
1246
1247        let parser = Parser::new(RdfFormat::Turtle);
1248        let quads = parser
1249            .parse_str_to_quads(turtle_data)
1250            .expect("comma object lists must parse");
1251
1252        assert_eq!(quads.len(), 3, "expected 3 triples, got {quads:?}");
1253        let objects: std::collections::HashSet<String> = quads
1254            .iter()
1255            .map(|q| q.to_triple().object().to_string())
1256            .collect();
1257        assert!(objects.iter().any(|o| o.contains("bob")));
1258        assert!(objects.iter().any(|o| o.contains("carol")));
1259        assert!(objects.iter().any(|o| o.contains("dave")));
1260    }
1261
1262    /// Regression test for the P0 finding: no support for blank-node
1263    /// property lists (`[ p o ]`) or RDF collections (`( a b c )`).
1264    #[test]
1265    fn test_turtle_blank_node_property_list_and_collection() {
1266        let turtle_data = r#"@prefix ex: <http://example.org/> .
1267ex:alice ex:address [ ex:city "Springfield" ; ex:zip "12345" ] ;
1268         ex:favorites ( ex:tea ex:coffee ex:cocoa ) ."#;
1269
1270        let parser = Parser::new(RdfFormat::Turtle);
1271        let quads = parser
1272            .parse_str_to_quads(turtle_data)
1273            .expect("blank-node property lists and collections must parse");
1274
1275        // ex:address triple + 2 property triples on the blank node = 3
1276        // ex:favorites triple + 3-element rdf:List (3 rdf:first + 3 rdf:rest, one being rdf:nil) = 4 triples
1277        // Just assert we got a reasonable number of triples and specific content is present.
1278        assert!(
1279            quads.len() >= 7,
1280            "expected at least 7 triples for property list + collection, got {} ({quads:?})",
1281            quads.len()
1282        );
1283
1284        let has_city = quads.iter().any(|q| {
1285            let t = q.to_triple();
1286            t.predicate().to_string().contains("city")
1287                && matches!(t.object(), Object::Literal(l) if l.value() == "Springfield")
1288        });
1289        assert!(has_city, "blank-node property list content missing");
1290
1291        let has_first = quads.iter().any(|q| {
1292            q.to_triple()
1293                .predicate()
1294                .to_string()
1295                .contains("rdf-syntax-ns#first")
1296        });
1297        assert!(
1298            has_first,
1299            "RDF collection must expand to rdf:first/rdf:rest"
1300        );
1301    }
1302
1303    /// Regression test for the P0 finding: multi-line `data.lines()`
1304    /// processing used to collapse newlines inside triple-quoted string
1305    /// literals into spaces, corrupting their content.
1306    #[test]
1307    fn test_turtle_triple_quoted_multiline_literal() {
1308        let turtle_data = "@prefix ex: <http://example.org/> .\nex:alice ex:bio \"\"\"Line one\nLine two\nLine three\"\"\" .";
1309
1310        let parser = Parser::new(RdfFormat::Turtle);
1311        let quads = parser
1312            .parse_str_to_quads(turtle_data)
1313            .expect("triple-quoted multi-line literals must parse");
1314
1315        assert_eq!(quads.len(), 1);
1316        let triple = quads[0].to_triple();
1317        if let Object::Literal(lit) = triple.object() {
1318            assert_eq!(lit.value(), "Line one\nLine two\nLine three");
1319        } else {
1320            panic!("Expected literal object");
1321        }
1322    }
1323
1324    /// Same triple-quoted multi-line literal regression, but through the
1325    /// TriG entry point (which delegates through the same real grammar).
1326    #[test]
1327    fn test_trig_triple_quoted_multiline_literal_in_named_graph() {
1328        let trig_data = "@prefix ex: <http://example.org/> .\nex:g1 { ex:alice ex:bio \"\"\"Line one\nLine two\"\"\" . }";
1329
1330        let parser = Parser::new(RdfFormat::TriG);
1331        let quads = parser
1332            .parse_str_to_quads(trig_data)
1333            .expect("triple-quoted multi-line literals must parse in TriG too");
1334
1335        assert_eq!(quads.len(), 1);
1336        assert!(!quads[0].is_default_graph());
1337        let triple = quads[0].to_triple();
1338        if let Object::Literal(lit) = triple.object() {
1339            assert_eq!(lit.value(), "Line one\nLine two");
1340        } else {
1341            panic!("Expected literal object");
1342        }
1343    }
1344
1345    #[test]
1346    fn test_turtle_parsing_base_iri() {
1347        let turtle_data = r#"@base <http://example.org/> .
1348<alice> <knows> <bob> ."#;
1349
1350        let parser = Parser::new(RdfFormat::Turtle);
1351        let result = parser.parse_str_to_quads(turtle_data);
1352
1353        assert!(result.is_ok());
1354        let quads = result.expect("should have value");
1355        assert_eq!(quads.len(), 1);
1356
1357        let triple = quads[0].to_triple();
1358        // IRIs should be resolved relative to base
1359        if let Subject::NamedNode(subj) = triple.subject() {
1360            assert!(subj.as_str().contains("example.org"));
1361        } else {
1362            panic!("Expected named node subject");
1363        }
1364    }
1365
1366    #[test]
1367    fn test_turtle_parsing_literals() {
1368        let turtle_data = r#"@prefix ex: <http://example.org/> .
1369ex:alice ex:name "Alice"@en .
1370ex:alice ex:age "30"^^<http://www.w3.org/2001/XMLSchema#integer> ."#;
1371
1372        let parser = Parser::new(RdfFormat::Turtle);
1373        let result = parser.parse_str_to_quads(turtle_data);
1374
1375        assert!(result.is_ok());
1376        let quads = result.expect("should have value");
1377        assert_eq!(quads.len(), 2);
1378
1379        // Check for language tag and datatype
1380        let triples: Vec<_> = quads.into_iter().map(|q| q.to_triple()).collect();
1381
1382        let mut found_lang_literal = false;
1383        let mut found_typed_literal = false;
1384
1385        for triple in triples {
1386            if let Object::Literal(literal) = triple.object() {
1387                if literal.language().is_some() {
1388                    found_lang_literal = true;
1389                    assert_eq!(literal.language(), Some("en"));
1390                } else {
1391                    let datatype = literal.datatype();
1392                    // Check for typed literal (not language-tagged and not plain string)
1393                    if datatype.as_str() != "http://www.w3.org/2001/XMLSchema#string"
1394                        && datatype.as_str()
1395                            != "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString"
1396                    {
1397                        found_typed_literal = true;
1398                        assert!(
1399                            datatype.as_str().contains("integer"),
1400                            "Expected integer datatype but got: {}",
1401                            datatype.as_str()
1402                        );
1403                    }
1404                }
1405            }
1406        }
1407
1408        assert!(found_lang_literal);
1409        assert!(found_typed_literal);
1410    }
1411
1412    #[test]
1413    fn test_parser_round_trip() {
1414        use crate::serializer::Serializer;
1415
1416        // Create a graph with various types of triples
1417        let mut original_graph = Graph::new();
1418
1419        let alice = NamedNode::new("http://example.org/alice").expect("valid IRI");
1420        let name_pred = NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("valid IRI");
1421        let name_literal = Literal::new("Alice Smith");
1422        original_graph.insert(Triple::new(alice.clone(), name_pred, name_literal));
1423
1424        let age_pred = NamedNode::new("http://xmlns.com/foaf/0.1/age").expect("valid IRI");
1425        let age_literal = Literal::new_typed("30", crate::vocab::xsd::INTEGER.clone());
1426        original_graph.insert(Triple::new(alice.clone(), age_pred, age_literal));
1427
1428        let desc_pred = NamedNode::new("http://example.org/description").expect("valid IRI");
1429        let desc_literal =
1430            Literal::new_lang("Une personne", "fr").expect("construction should succeed");
1431        original_graph.insert(Triple::new(alice, desc_pred, desc_literal));
1432
1433        // Serialize to N-Triples
1434        let serializer = Serializer::new(RdfFormat::NTriples);
1435        let ntriples = serializer
1436            .serialize_graph(&original_graph)
1437            .expect("operation should succeed");
1438
1439        // Parse back from N-Triples
1440        let parser = Parser::new(RdfFormat::NTriples);
1441        let quads = parser
1442            .parse_str_to_quads(&ntriples)
1443            .expect("operation should succeed");
1444
1445        // Convert back to graph
1446        let parsed_graph = Graph::from_iter(quads.into_iter().map(|q| q.to_triple()));
1447
1448        // Should have the same number of triples
1449        assert_eq!(original_graph.len(), parsed_graph.len());
1450
1451        // All original triples should be present in parsed graph
1452        for triple in original_graph.iter() {
1453            assert!(
1454                parsed_graph.contains(triple),
1455                "Parsed graph missing triple: {triple}"
1456            );
1457        }
1458    }
1459
1460    #[test]
1461    fn test_trig_parser() {
1462        let trig_data = r#"
1463@prefix ex: <http://example.org/> .
1464@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
1465
1466# Default graph
1467{
1468    ex:alice rdf:type ex:Person .
1469    ex:alice ex:name "Alice" .
1470}
1471
1472# Named graph
1473ex:graph1 {
1474    ex:bob rdf:type ex:Person .
1475    ex:bob ex:name "Bob" .
1476    ex:bob ex:age "30" .
1477}
1478"#;
1479
1480        let parser = Parser::new(RdfFormat::TriG);
1481        let quads = parser
1482            .parse_str_to_quads(trig_data)
1483            .expect("operation should succeed");
1484
1485        // Should parse all statements
1486        assert!(
1487            quads.len() >= 5,
1488            "Should parse at least 5 quads, got {}",
1489            quads.len()
1490        );
1491
1492        // Check that we have both default and named graph quads
1493        let default_graph_count = quads.iter().filter(|q| q.is_default_graph()).count();
1494        let named_graph_count = quads.len() - default_graph_count;
1495
1496        assert!(
1497            default_graph_count >= 2,
1498            "Should have at least 2 default graph quads, got {default_graph_count}"
1499        );
1500        assert!(
1501            named_graph_count >= 3,
1502            "Should have at least 3 named graph quads, got {named_graph_count}"
1503        );
1504
1505        // Verify specific content
1506        let alice_uri = "http://example.org/alice";
1507        let bob_uri = "http://example.org/bob";
1508        let person_uri = "http://example.org/Person";
1509
1510        // Check for Alice in default graph
1511        let alice_type_found = quads.iter().any(|q| {
1512            q.is_default_graph()
1513                && q.subject().to_string().contains(alice_uri)
1514                && q.object().to_string().contains(person_uri)
1515        });
1516        assert!(
1517            alice_type_found,
1518            "Should find Alice type assertion in default graph"
1519        );
1520
1521        // Check for Bob in named graph
1522        let bob_in_named_graph = quads
1523            .iter()
1524            .any(|q| !q.is_default_graph() && q.subject().to_string().contains(bob_uri));
1525        assert!(
1526            bob_in_named_graph,
1527            "Should find Bob statements in named graph"
1528        );
1529    }
1530
1531    #[test]
1532    fn test_trig_parser_prefixes() {
1533        let trig_data = r#"
1534@prefix ex: <http://example.org/> .
1535@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1536
1537ex:person1 foaf:name "John Doe" .
1538"#;
1539
1540        let parser = Parser::new(RdfFormat::TriG);
1541        let quads = parser
1542            .parse_str_to_quads(trig_data)
1543            .expect("operation should succeed");
1544
1545        assert!(!quads.is_empty(), "Should parse prefixed statements");
1546
1547        // Verify prefix expansion worked
1548        let expanded_found = quads.iter().any(|q| {
1549            q.subject()
1550                .to_string()
1551                .contains("http://example.org/person1")
1552                && q.predicate()
1553                    .to_string()
1554                    .contains("http://xmlns.com/foaf/0.1/name")
1555        });
1556        assert!(expanded_found, "Should expand prefixes correctly");
1557    }
1558
1559    #[test]
1560    fn test_jsonld_parser() {
1561        let jsonld_data = r#"{
1562    "@context": {
1563        "name": "http://xmlns.com/foaf/0.1/name",
1564        "Person": "http://schema.org/Person"
1565    },
1566    "@type": "Person",
1567    "@id": "http://example.org/john",
1568    "name": "John Doe"
1569}"#;
1570
1571        let parser = Parser::new(RdfFormat::JsonLd);
1572        let result = parser.parse_str_to_quads(jsonld_data);
1573
1574        match result {
1575            Ok(quads) => {
1576                println!("JSON-LD parsed {} quads:", quads.len());
1577                for quad in &quads {
1578                    println!("  {quad}");
1579                }
1580                assert!(!quads.is_empty(), "Should parse some quads from JSON-LD");
1581            }
1582            Err(e) => {
1583                // For now, just verify that the parser attempts to parse
1584                println!("JSON-LD parsing error (expected during development): {e}");
1585                // Don't fail the test yet as the implementation might need more work
1586            }
1587        }
1588    }
1589
1590    #[test]
1591    fn test_jsonld_parser_simple() {
1592        let jsonld_data = r#"{
1593    "@context": "http://schema.org/",
1594    "@type": "Person",
1595    "name": "Alice"
1596}"#;
1597
1598        let parser = Parser::new(RdfFormat::JsonLd);
1599        let result = parser.parse_str_to_quads(jsonld_data);
1600
1601        // For now, just verify the parser doesn't crash
1602        match result {
1603            Ok(quads) => {
1604                println!("Simple JSON-LD parsed {} quads", quads.len());
1605            }
1606            Err(e) => {
1607                println!("Simple JSON-LD parsing error: {e}");
1608                // Don't fail during development
1609            }
1610        }
1611    }
1612}