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
560        let mut end_quote_pos = None;
561        let mut escaped = false;
562        let chars: Vec<char> = token.chars().collect();
563
564        for (i, &ch) in chars.iter().enumerate().skip(1) {
565            if escaped {
566                escaped = false;
567                continue;
568            }
569
570            if ch == '\\' {
571                escaped = true;
572            } else if ch == '"' {
573                end_quote_pos = Some(i);
574                break;
575            }
576        }
577
578        let end_quote_pos =
579            end_quote_pos.ok_or_else(|| OxirsError::Parse("Unterminated literal".to_string()))?;
580
581        // Extract the literal value (without quotes) and unescape
582        let raw_value: String = chars[1..end_quote_pos].iter().collect();
583        let literal_value = self.unescape_literal_value(&raw_value)?;
584
585        // Check for language tag or datatype
586        let remaining = &token[end_quote_pos + 1..];
587
588        if let Some(lang_tag) = remaining.strip_prefix('@') {
589            // Language tag
590            let literal = Literal::new_lang(literal_value, lang_tag)?;
591            Ok(Object::Literal(literal))
592        } else if remaining.starts_with("^^<") && remaining.ends_with('>') {
593            // Datatype
594            let datatype_iri = &remaining[3..remaining.len() - 1];
595            let datatype = NamedNode::new(datatype_iri)?;
596            let literal = Literal::new_typed(literal_value, datatype);
597            Ok(Object::Literal(literal))
598        } else if remaining.is_empty() {
599            // Plain literal
600            let literal = Literal::new(literal_value);
601            Ok(Object::Literal(literal))
602        } else {
603            Err(OxirsError::Parse(format!(
604                "Invalid literal syntax: {token}"
605            )))
606        }
607    }
608
609    fn parse_trig<F>(&self, data: &str, mut handler: F) -> Result<()>
610    where
611        F: FnMut(Quad) -> Result<()>,
612    {
613        // Delegate to the real, oxttl-backed TriG grammar (same rationale as
614        // parse_turtle above: named-graph blocks, nested Turtle syntax, and
615        // multi-line statements are not safely handled by line splitting).
616        let mut internal_parser = crate::format::RdfParser::new(crate::format::RdfFormat::TriG);
617        if let Some(base) = &self.config.base_iri {
618            internal_parser = internal_parser.with_base_iri(base.clone());
619        }
620
621        for result in internal_parser.for_slice(data.as_bytes()) {
622            match result {
623                Ok(quad) => handler(quad)?,
624                Err(e) => {
625                    if self.config.ignore_errors {
626                        tracing::warn!("TriG parse error: {e}");
627                        continue;
628                    } else {
629                        return Err(OxirsError::Parse(format!("TriG parse error: {e}")));
630                    }
631                }
632            }
633        }
634
635        Ok(())
636    }
637
638    fn parse_nquads<F>(&self, data: &str, mut handler: F) -> Result<()>
639    where
640        F: FnMut(Quad) -> Result<()>,
641    {
642        for (line_num, line) in data.lines().enumerate() {
643            let line = line.trim();
644
645            // Skip empty lines and comments
646            if line.is_empty() || line.starts_with('#') {
647                continue;
648            }
649
650            // Parse the line into a quad
651            match self.parse_nquads_line(line) {
652                Ok(Some(quad)) => {
653                    handler(quad)?;
654                }
655                Ok(None) => {
656                    // Skip this line (e.g., blank line)
657                    continue;
658                }
659                Err(e) => {
660                    if self.config.ignore_errors {
661                        tracing::warn!("Parse error on line {}: {}", line_num + 1, e);
662                        continue;
663                    } else {
664                        return Err(OxirsError::Parse(format!(
665                            "Parse error on line {}: {}",
666                            line_num + 1,
667                            e
668                        )));
669                    }
670                }
671            }
672        }
673
674        Ok(())
675    }
676
677    pub fn parse_nquads_line(&self, line: &str) -> Result<Option<Quad>> {
678        // N-Quads parser - parse line like: <s> <p> "o" <g> .
679        let line = line.trim();
680
681        if line.is_empty() || line.starts_with('#') {
682            return Ok(None);
683        }
684
685        // Find the final period
686        if !line.ends_with('.') {
687            return Err(OxirsError::Parse("Line must end with '.'".to_string()));
688        }
689
690        let line = &line[..line.len() - 1].trim(); // Remove trailing period and whitespace
691
692        // Split into tokens respecting quoted strings
693        let tokens = self.tokenize_ntriples_line(line)?;
694
695        if tokens.len() != 4 {
696            return Err(OxirsError::Parse(format!(
697                "Expected 4 tokens (subject, predicate, object, graph), found {}",
698                tokens.len()
699            )));
700        }
701
702        // Parse subject
703        let subject = self.parse_subject(&tokens[0])?;
704
705        // Parse predicate
706        let predicate = self.parse_predicate(&tokens[1])?;
707
708        // Parse object
709        let object = self.parse_object(&tokens[2])?;
710
711        // Parse graph name
712        let graph_name = self.parse_graph_name(&tokens[3])?;
713
714        let quad = Quad::new(subject, predicate, object, graph_name);
715
716        Ok(Some(quad))
717    }
718
719    fn parse_graph_name(&self, token: &str) -> Result<GraphName> {
720        if token.starts_with('<') && token.ends_with('>') {
721            let iri = &token[1..token.len() - 1];
722            let named_node = NamedNode::new(iri)?;
723            Ok(GraphName::NamedNode(named_node))
724        } else if token.starts_with("_:") {
725            let blank_node = BlankNode::new(token)?;
726            Ok(GraphName::BlankNode(blank_node))
727        } else {
728            Err(OxirsError::Parse(format!(
729                "Invalid graph name: {token}. Must be IRI or blank node"
730            )))
731        }
732    }
733
734    fn parse_rdfxml<F>(&self, data: &str, mut handler: F) -> Result<()>
735    where
736        F: FnMut(Quad) -> Result<()>,
737    {
738        use crate::rdfxml::wrapper::parse_rdfxml;
739        use std::io::Cursor;
740
741        // Parse RDF/XML data using the wrapper
742        let reader = Cursor::new(data.as_bytes());
743        let base_iri = self.config.base_iri.as_deref();
744        let quads = parse_rdfxml(reader, base_iri, self.config.ignore_errors)?;
745
746        // Process each quad through the handler
747        for quad in quads {
748            handler(quad)?;
749        }
750
751        Ok(())
752    }
753
754    fn parse_jsonld<F>(&self, data: &str, mut handler: F) -> Result<()>
755    where
756        F: FnMut(Quad) -> Result<()>,
757    {
758        // Basic JSON-LD parser implementation using existing jsonld module
759        use crate::jsonld::to_rdf::JsonLdParser;
760
761        let parser = JsonLdParser::new();
762        let parser = if let Some(base_iri) = &self.config.base_iri {
763            parser
764                .with_base_iri(base_iri.clone())
765                .map_err(|e| OxirsError::Parse(format!("Invalid base IRI: {e}")))?
766        } else {
767            parser
768        };
769
770        // Parse JSON-LD data into quads
771        for result in parser.for_slice(data.as_bytes()) {
772            match result {
773                Ok(quad) => handler(quad)?,
774                Err(e) => {
775                    if self.config.ignore_errors {
776                        tracing::warn!("JSON-LD parse error: {}", e);
777                        continue;
778                    } else {
779                        return Err(OxirsError::Parse(format!("JSON-LD parse error: {e}")));
780                    }
781                }
782            }
783        }
784
785        Ok(())
786    }
787
788    /// Unescape special characters in literal values
789    fn unescape_literal_value(&self, value: &str) -> Result<String> {
790        let mut result = String::new();
791        let mut chars = value.chars();
792
793        while let Some(c) = chars.next() {
794            if c == '\\' {
795                match chars.next() {
796                    Some('"') => result.push('"'),
797                    Some('\\') => result.push('\\'),
798                    Some('n') => result.push('\n'),
799                    Some('r') => result.push('\r'),
800                    Some('t') => result.push('\t'),
801                    Some('u') => {
802                        // Parse \uHHHH Unicode escape
803                        let hex_chars: String = chars.by_ref().take(4).collect();
804                        if hex_chars.len() != 4 {
805                            return Err(OxirsError::Parse(
806                                "Invalid Unicode escape sequence \\uHHHH - expected 4 hex digits"
807                                    .to_string(),
808                            ));
809                        }
810                        let code_point = u32::from_str_radix(&hex_chars, 16).map_err(|_| {
811                            OxirsError::Parse(
812                                "Invalid hex digits in Unicode escape sequence".to_string(),
813                            )
814                        })?;
815                        let unicode_char = char::from_u32(code_point).ok_or_else(|| {
816                            OxirsError::Parse("Invalid Unicode code point".to_string())
817                        })?;
818                        result.push(unicode_char);
819                    }
820                    Some('U') => {
821                        // Parse \UHHHHHHHH Unicode escape
822                        let hex_chars: String = chars.by_ref().take(8).collect();
823                        if hex_chars.len() != 8 {
824                            return Err(OxirsError::Parse(
825                                "Invalid Unicode escape sequence \\UHHHHHHHH - expected 8 hex digits".to_string()
826                            ));
827                        }
828                        let code_point = u32::from_str_radix(&hex_chars, 16).map_err(|_| {
829                            OxirsError::Parse(
830                                "Invalid hex digits in Unicode escape sequence".to_string(),
831                            )
832                        })?;
833                        let unicode_char = char::from_u32(code_point).ok_or_else(|| {
834                            OxirsError::Parse("Invalid Unicode code point".to_string())
835                        })?;
836                        result.push(unicode_char);
837                    }
838                    Some(other) => {
839                        return Err(OxirsError::Parse(format!(
840                            "Invalid escape sequence \\{other}"
841                        )));
842                    }
843                    None => {
844                        return Err(OxirsError::Parse(
845                            "Incomplete escape sequence at end of literal".to_string(),
846                        ));
847                    }
848                }
849            } else {
850                result.push(c);
851            }
852        }
853
854        Ok(result)
855    }
856
857    // Native parsing implementation complete - no external dependencies needed
858}
859
860/// Convenience function to detect RDF format from content
861pub fn detect_format_from_content(content: &str) -> Option<RdfFormat> {
862    let content = content.trim();
863
864    // Check for XML-like content (RDF/XML)
865    if content.starts_with("<?xml")
866        || content.starts_with("<rdf:RDF")
867        || content.starts_with("<RDF")
868    {
869        return Some(RdfFormat::RdfXml);
870    }
871
872    // Check for JSON-LD
873    if content.starts_with('{') && (content.contains("@context") || content.contains("@type")) {
874        return Some(RdfFormat::JsonLd);
875    }
876
877    // Check for Turtle syntax elements first (has priority over N-Quads/N-Triples)
878    if content.contains("@prefix") || content.contains("@base") || content.contains(';') {
879        return Some(RdfFormat::Turtle);
880    }
881
882    // Check for TriG (named graphs syntax)
883    if content.contains('{') && content.contains('}') {
884        return Some(RdfFormat::TriG);
885    }
886
887    // Count tokens in first meaningful line to distinguish N-Quads vs N-Triples
888    for line in content.lines() {
889        let line = line.trim();
890        if !line.is_empty() && !line.starts_with('#') {
891            let parts: Vec<&str> = line.split_whitespace().collect();
892            if parts.len() == 4 && parts[3] == "." {
893                // Exactly 4 parts (s p o .) - N-Triples
894                return Some(RdfFormat::NTriples);
895            } else if parts.len() == 5 && parts[4] == "." {
896                // Exactly 5 parts (s p o g .) - N-Quads
897                return Some(RdfFormat::NQuads);
898            } else if parts.len() >= 3 && parts[parts.len() - 1] == "." {
899                // Fallback: assume N-Triples for basic triple pattern
900                return Some(RdfFormat::NTriples);
901            }
902            break; // Only check first meaningful line
903        }
904    }
905
906    None
907}
908
909#[cfg(test)]
910mod tests {
911    use super::*;
912    use crate::model::graph::Graph;
913
914    #[test]
915    fn test_format_detection_from_extension() {
916        assert_eq!(RdfFormat::from_extension("ttl"), Some(RdfFormat::Turtle));
917        assert_eq!(RdfFormat::from_extension("turtle"), Some(RdfFormat::Turtle));
918        assert_eq!(RdfFormat::from_extension("nt"), Some(RdfFormat::NTriples));
919        assert_eq!(
920            RdfFormat::from_extension("ntriples"),
921            Some(RdfFormat::NTriples)
922        );
923        assert_eq!(RdfFormat::from_extension("trig"), Some(RdfFormat::TriG));
924        assert_eq!(RdfFormat::from_extension("nq"), Some(RdfFormat::NQuads));
925        assert_eq!(RdfFormat::from_extension("rdf"), Some(RdfFormat::RdfXml));
926        assert_eq!(RdfFormat::from_extension("jsonld"), Some(RdfFormat::JsonLd));
927        assert_eq!(RdfFormat::from_extension("unknown"), None);
928    }
929
930    #[test]
931    fn test_format_properties() {
932        assert_eq!(RdfFormat::Turtle.media_type(), "text/turtle");
933        assert_eq!(RdfFormat::NTriples.extension(), "nt");
934        assert!(RdfFormat::TriG.supports_quads());
935        assert!(!RdfFormat::Turtle.supports_quads());
936    }
937
938    #[test]
939    fn test_format_detection_from_content() {
940        // XML content
941        let xml_content = "<?xml version=\"1.0\"?>\n<rdf:RDF>";
942        assert_eq!(
943            detect_format_from_content(xml_content),
944            Some(RdfFormat::RdfXml)
945        );
946
947        // JSON-LD content
948        let jsonld_content = r#"{"@context": "http://example.org", "@type": "Person"}"#;
949        assert_eq!(
950            detect_format_from_content(jsonld_content),
951            Some(RdfFormat::JsonLd)
952        );
953
954        // Turtle content
955        let turtle_content = "@prefix foaf: <http://xmlns.com/foaf/0.1/> .";
956        assert_eq!(
957            detect_format_from_content(turtle_content),
958            Some(RdfFormat::Turtle)
959        );
960
961        // N-Triples content
962        let ntriples_content = "<http://example.org/s> <http://example.org/p> \"object\" .";
963        assert_eq!(
964            detect_format_from_content(ntriples_content),
965            Some(RdfFormat::NTriples)
966        );
967    }
968
969    #[test]
970    fn test_ntriples_parsing_simple() {
971        let ntriples_data = r#"<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" .
972<http://example.org/alice> <http://xmlns.com/foaf/0.1/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
973_:person1 <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> ."#;
974
975        let parser = Parser::new(RdfFormat::NTriples);
976        let result = parser.parse_str_to_quads(ntriples_data);
977
978        assert!(result.is_ok());
979        let quads = result.expect("should have value");
980        assert_eq!(quads.len(), 3);
981
982        // Check that all quads are in the default graph
983        for quad in &quads {
984            assert!(quad.is_default_graph());
985        }
986
987        // Convert to triples for easier checking
988        let triples: Vec<_> = quads.into_iter().map(|q| q.to_triple()).collect();
989
990        // Check first triple
991        let alice_iri = NamedNode::new("http://example.org/alice").expect("valid IRI");
992        let name_pred = NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("valid IRI");
993        let name_literal = Literal::new("Alice Smith");
994        let expected_triple1 = Triple::new(alice_iri.clone(), name_pred, name_literal);
995        assert!(triples.contains(&expected_triple1));
996
997        // Check typed literal triple
998        let age_pred = NamedNode::new("http://xmlns.com/foaf/0.1/age").expect("valid IRI");
999        let integer_type =
1000            NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").expect("valid IRI");
1001        let age_literal = Literal::new_typed("30", integer_type);
1002        let expected_triple2 = Triple::new(alice_iri, age_pred, age_literal);
1003        assert!(triples.contains(&expected_triple2));
1004
1005        // Check blank node triple
1006        let blank_node = BlankNode::new("_:person1").expect("valid blank node id");
1007        let knows_pred = NamedNode::new("http://xmlns.com/foaf/0.1/knows").expect("valid IRI");
1008        let bob_iri = NamedNode::new("http://example.org/bob").expect("valid IRI");
1009        let expected_triple3 = Triple::new(blank_node, knows_pred, bob_iri);
1010        assert!(triples.contains(&expected_triple3));
1011    }
1012
1013    #[test]
1014    fn test_ntriples_parsing_language_tag() {
1015        let ntriples_data =
1016            r#"<http://example.org/alice> <http://example.org/description> "Une personne"@fr ."#;
1017
1018        let parser = Parser::new(RdfFormat::NTriples);
1019        let result = parser.parse_str_to_quads(ntriples_data);
1020
1021        assert!(result.is_ok());
1022        let quads = result.expect("should have value");
1023        assert_eq!(quads.len(), 1);
1024
1025        let triple = quads[0].to_triple();
1026        if let Object::Literal(literal) = triple.object() {
1027            assert_eq!(literal.value(), "Une personne");
1028            assert_eq!(literal.language(), Some("fr"));
1029            assert!(literal.is_lang_string());
1030        } else {
1031            panic!("Expected literal object");
1032        }
1033    }
1034
1035    #[test]
1036    fn test_ntriples_parsing_escaped_literals() {
1037        let ntriples_data = r#"<http://example.org/test> <http://example.org/desc> "Text with \"quotes\" and \n newlines" ."#;
1038
1039        let parser = Parser::new(RdfFormat::NTriples);
1040        let result = parser.parse_str_to_quads(ntriples_data);
1041
1042        if let Err(e) = &result {
1043            println!("Parse error: {e}");
1044        }
1045        assert!(result.is_ok(), "Parse failed: {result:?}");
1046
1047        let quads = result.expect("should have value");
1048        assert_eq!(quads.len(), 1);
1049
1050        let triple = quads[0].to_triple();
1051        if let Object::Literal(literal) = triple.object() {
1052            assert!(literal.value().contains("\"quotes\""));
1053            assert!(literal.value().contains("\n"));
1054        } else {
1055            panic!("Expected literal object");
1056        }
1057    }
1058
1059    #[test]
1060    fn test_ntriples_parsing_comments_and_empty_lines() {
1061        let ntriples_data = r#"
1062# This is a comment
1063<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" .
1064
1065# Another comment
1066<http://example.org/bob> <http://xmlns.com/foaf/0.1/name> "Bob Jones" .
1067"#;
1068
1069        let parser = Parser::new(RdfFormat::NTriples);
1070        let result = parser.parse_str_to_quads(ntriples_data);
1071
1072        assert!(result.is_ok());
1073        let quads = result.expect("should have value");
1074        assert_eq!(quads.len(), 2);
1075    }
1076
1077    #[test]
1078    fn test_ntriples_parsing_error_handling() {
1079        // Test invalid syntax
1080        let invalid_data = "invalid ntriples data";
1081        let parser = Parser::new(RdfFormat::NTriples);
1082        let result = parser.parse_str_to_quads(invalid_data);
1083        assert!(result.is_err());
1084
1085        // Test error tolerance
1086        let mixed_data = r#"<http://example.org/valid> <http://example.org/pred> "Valid triple" .
1087invalid line here
1088<http://example.org/valid2> <http://example.org/pred> "Another valid triple" ."#;
1089
1090        let parser_strict = Parser::new(RdfFormat::NTriples);
1091        let result_strict = parser_strict.parse_str_to_quads(mixed_data);
1092        assert!(result_strict.is_err());
1093
1094        let parser_tolerant = Parser::new(RdfFormat::NTriples).with_error_tolerance(true);
1095        let result_tolerant = parser_tolerant.parse_str_to_quads(mixed_data);
1096        assert!(result_tolerant.is_ok());
1097        let quads = result_tolerant.expect("tolerant parse should succeed");
1098        assert_eq!(quads.len(), 2); // Should parse the two valid triples
1099    }
1100
1101    #[test]
1102    fn test_nquads_parsing() {
1103        let nquads_data = r#"<http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice Smith" <http://example.org/graph1> .
1104<http://example.org/alice> <http://xmlns.com/foaf/0.1/age> "30"^^<http://www.w3.org/2001/XMLSchema#integer> <http://example.org/graph2> .
1105_:person1 <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> _:graph1 ."#;
1106
1107        let parser = Parser::new(RdfFormat::NQuads);
1108        let result = parser.parse_str_to_quads(nquads_data);
1109
1110        assert!(result.is_ok());
1111        let quads = result.expect("should have value");
1112        assert_eq!(quads.len(), 3);
1113
1114        // Check that quads have proper graph names
1115        let first_quad = &quads[0];
1116        assert!(!first_quad.is_default_graph());
1117
1118        // Check that we can extract graph names
1119        if let GraphName::NamedNode(graph_name) = first_quad.graph_name() {
1120            assert!(graph_name.as_str().contains("example.org"));
1121        } else {
1122            panic!("Expected named graph");
1123        }
1124    }
1125
1126    #[test]
1127    fn test_turtle_parsing_basic() {
1128        let turtle_data = r#"@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1129@prefix ex: <http://example.org/> .
1130
1131ex:alice foaf:name "Alice Smith" .
1132ex:alice foaf:age "30"^^<http://www.w3.org/2001/XMLSchema#integer> .
1133ex:alice foaf:knows ex:bob ."#;
1134
1135        let parser = Parser::new(RdfFormat::Turtle);
1136        let result = parser.parse_str_to_quads(turtle_data);
1137
1138        assert!(result.is_ok());
1139        let quads = result.expect("should have value");
1140        assert_eq!(quads.len(), 3);
1141
1142        // All quads should be in default graph
1143        for quad in &quads {
1144            assert!(quad.is_default_graph());
1145        }
1146    }
1147
1148    #[test]
1149    fn test_turtle_parsing_prefixes() {
1150        let turtle_data = r#"@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1151foaf:Person a foaf:Person ."#;
1152
1153        let parser = Parser::new(RdfFormat::Turtle);
1154        let result = parser.parse_str_to_quads(turtle_data);
1155
1156        assert!(result.is_ok());
1157        let quads = result.expect("should have value");
1158        assert_eq!(quads.len(), 1);
1159
1160        let triple = quads[0].to_triple();
1161        // Should expand foaf:Person to full IRI
1162        if let Subject::NamedNode(subj) = triple.subject() {
1163            assert!(subj.as_str().contains("xmlns.com/foaf"));
1164        } else {
1165            panic!("Expected named node subject");
1166        }
1167
1168        // Predicate should be rdf:type (from 'a')
1169        if let Predicate::NamedNode(pred) = triple.predicate() {
1170            assert!(pred.as_str().contains("rdf-syntax-ns#type"));
1171        } else {
1172            panic!("Expected named node predicate");
1173        }
1174    }
1175
1176    #[test]
1177    fn test_turtle_parsing_abbreviated_syntax() {
1178        let turtle_data = r#"@prefix ex: <http://example.org/> .
1179@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1180
1181ex:alice foaf:name "Alice" ;
1182         foaf:age "30" ."#;
1183
1184        let parser = Parser::new(RdfFormat::Turtle);
1185        let result = parser.parse_str_to_quads(turtle_data);
1186
1187        assert!(result.is_ok());
1188        let quads = result.expect("should have value");
1189        assert_eq!(quads.len(), 2);
1190
1191        // Both triples should have the same subject
1192        let subjects: Vec<_> = quads
1193            .iter()
1194            .map(|q| q.to_triple().subject().clone())
1195            .collect();
1196        assert_eq!(subjects[0], subjects[1]);
1197    }
1198
1199    /// Regression test for the P0 finding: semicolon-splitting used to be
1200    /// done on the raw accumulated statement *before* any quote-aware
1201    /// tokenization, so a literal value containing a `;` corrupted the
1202    /// triple (spurious extra/garbled triples or parse errors). The real
1203    /// oxttl-backed grammar must treat `;` inside a string literal as plain
1204    /// literal content, not a predicate-object-list separator.
1205    #[test]
1206    fn test_turtle_semicolon_inside_literal_not_split() {
1207        let turtle_data = r#"@prefix ex: <http://example.org/> .
1208ex:alice ex:bio "Loves cats; dogs; and turtles" ;
1209         ex:name "Alice" ."#;
1210
1211        let parser = Parser::new(RdfFormat::Turtle);
1212        let quads = parser
1213            .parse_str_to_quads(turtle_data)
1214            .expect("semicolon inside a literal must not corrupt parsing");
1215
1216        assert_eq!(quads.len(), 2, "expected exactly 2 triples, got {quads:?}");
1217
1218        let bio_triple = quads
1219            .iter()
1220            .map(|q| q.to_triple())
1221            .find(|t| t.predicate().to_string().contains("bio"))
1222            .expect("bio triple should be present");
1223        if let Object::Literal(lit) = bio_triple.object() {
1224            assert_eq!(lit.value(), "Loves cats; dogs; and turtles");
1225        } else {
1226            panic!("Expected literal object for ex:bio");
1227        }
1228    }
1229
1230    /// Regression test for the P0 finding: the hand-rolled state machine had
1231    /// no support for comma-separated object lists (`predicate obj1, obj2`).
1232    #[test]
1233    fn test_turtle_comma_object_list() {
1234        let turtle_data = r#"@prefix ex: <http://example.org/> .
1235ex:alice ex:knows ex:bob, ex:carol, ex:dave ."#;
1236
1237        let parser = Parser::new(RdfFormat::Turtle);
1238        let quads = parser
1239            .parse_str_to_quads(turtle_data)
1240            .expect("comma object lists must parse");
1241
1242        assert_eq!(quads.len(), 3, "expected 3 triples, got {quads:?}");
1243        let objects: std::collections::HashSet<String> = quads
1244            .iter()
1245            .map(|q| q.to_triple().object().to_string())
1246            .collect();
1247        assert!(objects.iter().any(|o| o.contains("bob")));
1248        assert!(objects.iter().any(|o| o.contains("carol")));
1249        assert!(objects.iter().any(|o| o.contains("dave")));
1250    }
1251
1252    /// Regression test for the P0 finding: no support for blank-node
1253    /// property lists (`[ p o ]`) or RDF collections (`( a b c )`).
1254    #[test]
1255    fn test_turtle_blank_node_property_list_and_collection() {
1256        let turtle_data = r#"@prefix ex: <http://example.org/> .
1257ex:alice ex:address [ ex:city "Springfield" ; ex:zip "12345" ] ;
1258         ex:favorites ( ex:tea ex:coffee ex:cocoa ) ."#;
1259
1260        let parser = Parser::new(RdfFormat::Turtle);
1261        let quads = parser
1262            .parse_str_to_quads(turtle_data)
1263            .expect("blank-node property lists and collections must parse");
1264
1265        // ex:address triple + 2 property triples on the blank node = 3
1266        // ex:favorites triple + 3-element rdf:List (3 rdf:first + 3 rdf:rest, one being rdf:nil) = 4 triples
1267        // Just assert we got a reasonable number of triples and specific content is present.
1268        assert!(
1269            quads.len() >= 7,
1270            "expected at least 7 triples for property list + collection, got {} ({quads:?})",
1271            quads.len()
1272        );
1273
1274        let has_city = quads.iter().any(|q| {
1275            let t = q.to_triple();
1276            t.predicate().to_string().contains("city")
1277                && matches!(t.object(), Object::Literal(l) if l.value() == "Springfield")
1278        });
1279        assert!(has_city, "blank-node property list content missing");
1280
1281        let has_first = quads.iter().any(|q| {
1282            q.to_triple()
1283                .predicate()
1284                .to_string()
1285                .contains("rdf-syntax-ns#first")
1286        });
1287        assert!(
1288            has_first,
1289            "RDF collection must expand to rdf:first/rdf:rest"
1290        );
1291    }
1292
1293    /// Regression test for the P0 finding: multi-line `data.lines()`
1294    /// processing used to collapse newlines inside triple-quoted string
1295    /// literals into spaces, corrupting their content.
1296    #[test]
1297    fn test_turtle_triple_quoted_multiline_literal() {
1298        let turtle_data = "@prefix ex: <http://example.org/> .\nex:alice ex:bio \"\"\"Line one\nLine two\nLine three\"\"\" .";
1299
1300        let parser = Parser::new(RdfFormat::Turtle);
1301        let quads = parser
1302            .parse_str_to_quads(turtle_data)
1303            .expect("triple-quoted multi-line literals must parse");
1304
1305        assert_eq!(quads.len(), 1);
1306        let triple = quads[0].to_triple();
1307        if let Object::Literal(lit) = triple.object() {
1308            assert_eq!(lit.value(), "Line one\nLine two\nLine three");
1309        } else {
1310            panic!("Expected literal object");
1311        }
1312    }
1313
1314    /// Same triple-quoted multi-line literal regression, but through the
1315    /// TriG entry point (which delegates through the same real grammar).
1316    #[test]
1317    fn test_trig_triple_quoted_multiline_literal_in_named_graph() {
1318        let trig_data = "@prefix ex: <http://example.org/> .\nex:g1 { ex:alice ex:bio \"\"\"Line one\nLine two\"\"\" . }";
1319
1320        let parser = Parser::new(RdfFormat::TriG);
1321        let quads = parser
1322            .parse_str_to_quads(trig_data)
1323            .expect("triple-quoted multi-line literals must parse in TriG too");
1324
1325        assert_eq!(quads.len(), 1);
1326        assert!(!quads[0].is_default_graph());
1327        let triple = quads[0].to_triple();
1328        if let Object::Literal(lit) = triple.object() {
1329            assert_eq!(lit.value(), "Line one\nLine two");
1330        } else {
1331            panic!("Expected literal object");
1332        }
1333    }
1334
1335    #[test]
1336    fn test_turtle_parsing_base_iri() {
1337        let turtle_data = r#"@base <http://example.org/> .
1338<alice> <knows> <bob> ."#;
1339
1340        let parser = Parser::new(RdfFormat::Turtle);
1341        let result = parser.parse_str_to_quads(turtle_data);
1342
1343        assert!(result.is_ok());
1344        let quads = result.expect("should have value");
1345        assert_eq!(quads.len(), 1);
1346
1347        let triple = quads[0].to_triple();
1348        // IRIs should be resolved relative to base
1349        if let Subject::NamedNode(subj) = triple.subject() {
1350            assert!(subj.as_str().contains("example.org"));
1351        } else {
1352            panic!("Expected named node subject");
1353        }
1354    }
1355
1356    #[test]
1357    fn test_turtle_parsing_literals() {
1358        let turtle_data = r#"@prefix ex: <http://example.org/> .
1359ex:alice ex:name "Alice"@en .
1360ex:alice ex:age "30"^^<http://www.w3.org/2001/XMLSchema#integer> ."#;
1361
1362        let parser = Parser::new(RdfFormat::Turtle);
1363        let result = parser.parse_str_to_quads(turtle_data);
1364
1365        assert!(result.is_ok());
1366        let quads = result.expect("should have value");
1367        assert_eq!(quads.len(), 2);
1368
1369        // Check for language tag and datatype
1370        let triples: Vec<_> = quads.into_iter().map(|q| q.to_triple()).collect();
1371
1372        let mut found_lang_literal = false;
1373        let mut found_typed_literal = false;
1374
1375        for triple in triples {
1376            if let Object::Literal(literal) = triple.object() {
1377                if literal.language().is_some() {
1378                    found_lang_literal = true;
1379                    assert_eq!(literal.language(), Some("en"));
1380                } else {
1381                    let datatype = literal.datatype();
1382                    // Check for typed literal (not language-tagged and not plain string)
1383                    if datatype.as_str() != "http://www.w3.org/2001/XMLSchema#string"
1384                        && datatype.as_str()
1385                            != "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString"
1386                    {
1387                        found_typed_literal = true;
1388                        assert!(
1389                            datatype.as_str().contains("integer"),
1390                            "Expected integer datatype but got: {}",
1391                            datatype.as_str()
1392                        );
1393                    }
1394                }
1395            }
1396        }
1397
1398        assert!(found_lang_literal);
1399        assert!(found_typed_literal);
1400    }
1401
1402    #[test]
1403    fn test_parser_round_trip() {
1404        use crate::serializer::Serializer;
1405
1406        // Create a graph with various types of triples
1407        let mut original_graph = Graph::new();
1408
1409        let alice = NamedNode::new("http://example.org/alice").expect("valid IRI");
1410        let name_pred = NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("valid IRI");
1411        let name_literal = Literal::new("Alice Smith");
1412        original_graph.insert(Triple::new(alice.clone(), name_pred, name_literal));
1413
1414        let age_pred = NamedNode::new("http://xmlns.com/foaf/0.1/age").expect("valid IRI");
1415        let age_literal = Literal::new_typed("30", crate::vocab::xsd::INTEGER.clone());
1416        original_graph.insert(Triple::new(alice.clone(), age_pred, age_literal));
1417
1418        let desc_pred = NamedNode::new("http://example.org/description").expect("valid IRI");
1419        let desc_literal =
1420            Literal::new_lang("Une personne", "fr").expect("construction should succeed");
1421        original_graph.insert(Triple::new(alice, desc_pred, desc_literal));
1422
1423        // Serialize to N-Triples
1424        let serializer = Serializer::new(RdfFormat::NTriples);
1425        let ntriples = serializer
1426            .serialize_graph(&original_graph)
1427            .expect("operation should succeed");
1428
1429        // Parse back from N-Triples
1430        let parser = Parser::new(RdfFormat::NTriples);
1431        let quads = parser
1432            .parse_str_to_quads(&ntriples)
1433            .expect("operation should succeed");
1434
1435        // Convert back to graph
1436        let parsed_graph = Graph::from_iter(quads.into_iter().map(|q| q.to_triple()));
1437
1438        // Should have the same number of triples
1439        assert_eq!(original_graph.len(), parsed_graph.len());
1440
1441        // All original triples should be present in parsed graph
1442        for triple in original_graph.iter() {
1443            assert!(
1444                parsed_graph.contains(triple),
1445                "Parsed graph missing triple: {triple}"
1446            );
1447        }
1448    }
1449
1450    #[test]
1451    fn test_trig_parser() {
1452        let trig_data = r#"
1453@prefix ex: <http://example.org/> .
1454@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
1455
1456# Default graph
1457{
1458    ex:alice rdf:type ex:Person .
1459    ex:alice ex:name "Alice" .
1460}
1461
1462# Named graph
1463ex:graph1 {
1464    ex:bob rdf:type ex:Person .
1465    ex:bob ex:name "Bob" .
1466    ex:bob ex:age "30" .
1467}
1468"#;
1469
1470        let parser = Parser::new(RdfFormat::TriG);
1471        let quads = parser
1472            .parse_str_to_quads(trig_data)
1473            .expect("operation should succeed");
1474
1475        // Should parse all statements
1476        assert!(
1477            quads.len() >= 5,
1478            "Should parse at least 5 quads, got {}",
1479            quads.len()
1480        );
1481
1482        // Check that we have both default and named graph quads
1483        let default_graph_count = quads.iter().filter(|q| q.is_default_graph()).count();
1484        let named_graph_count = quads.len() - default_graph_count;
1485
1486        assert!(
1487            default_graph_count >= 2,
1488            "Should have at least 2 default graph quads, got {default_graph_count}"
1489        );
1490        assert!(
1491            named_graph_count >= 3,
1492            "Should have at least 3 named graph quads, got {named_graph_count}"
1493        );
1494
1495        // Verify specific content
1496        let alice_uri = "http://example.org/alice";
1497        let bob_uri = "http://example.org/bob";
1498        let person_uri = "http://example.org/Person";
1499
1500        // Check for Alice in default graph
1501        let alice_type_found = quads.iter().any(|q| {
1502            q.is_default_graph()
1503                && q.subject().to_string().contains(alice_uri)
1504                && q.object().to_string().contains(person_uri)
1505        });
1506        assert!(
1507            alice_type_found,
1508            "Should find Alice type assertion in default graph"
1509        );
1510
1511        // Check for Bob in named graph
1512        let bob_in_named_graph = quads
1513            .iter()
1514            .any(|q| !q.is_default_graph() && q.subject().to_string().contains(bob_uri));
1515        assert!(
1516            bob_in_named_graph,
1517            "Should find Bob statements in named graph"
1518        );
1519    }
1520
1521    #[test]
1522    fn test_trig_parser_prefixes() {
1523        let trig_data = r#"
1524@prefix ex: <http://example.org/> .
1525@prefix foaf: <http://xmlns.com/foaf/0.1/> .
1526
1527ex:person1 foaf:name "John Doe" .
1528"#;
1529
1530        let parser = Parser::new(RdfFormat::TriG);
1531        let quads = parser
1532            .parse_str_to_quads(trig_data)
1533            .expect("operation should succeed");
1534
1535        assert!(!quads.is_empty(), "Should parse prefixed statements");
1536
1537        // Verify prefix expansion worked
1538        let expanded_found = quads.iter().any(|q| {
1539            q.subject()
1540                .to_string()
1541                .contains("http://example.org/person1")
1542                && q.predicate()
1543                    .to_string()
1544                    .contains("http://xmlns.com/foaf/0.1/name")
1545        });
1546        assert!(expanded_found, "Should expand prefixes correctly");
1547    }
1548
1549    #[test]
1550    fn test_jsonld_parser() {
1551        let jsonld_data = r#"{
1552    "@context": {
1553        "name": "http://xmlns.com/foaf/0.1/name",
1554        "Person": "http://schema.org/Person"
1555    },
1556    "@type": "Person",
1557    "@id": "http://example.org/john",
1558    "name": "John Doe"
1559}"#;
1560
1561        let parser = Parser::new(RdfFormat::JsonLd);
1562        let result = parser.parse_str_to_quads(jsonld_data);
1563
1564        match result {
1565            Ok(quads) => {
1566                println!("JSON-LD parsed {} quads:", quads.len());
1567                for quad in &quads {
1568                    println!("  {quad}");
1569                }
1570                assert!(!quads.is_empty(), "Should parse some quads from JSON-LD");
1571            }
1572            Err(e) => {
1573                // For now, just verify that the parser attempts to parse
1574                println!("JSON-LD parsing error (expected during development): {e}");
1575                // Don't fail the test yet as the implementation might need more work
1576            }
1577        }
1578    }
1579
1580    #[test]
1581    fn test_jsonld_parser_simple() {
1582        let jsonld_data = r#"{
1583    "@context": "http://schema.org/",
1584    "@type": "Person",
1585    "name": "Alice"
1586}"#;
1587
1588        let parser = Parser::new(RdfFormat::JsonLd);
1589        let result = parser.parse_str_to_quads(jsonld_data);
1590
1591        // For now, just verify the parser doesn't crash
1592        match result {
1593            Ok(quads) => {
1594                println!("Simple JSON-LD parsed {} quads", quads.len());
1595            }
1596            Err(e) => {
1597                println!("Simple JSON-LD parsing error: {e}");
1598                // Don't fail during development
1599            }
1600        }
1601    }
1602}