Skip to main content

oxirs_core/format/
mod.rs

1//! RDF Format Support Module
2//!
3//! Phase 3 of OxiGraph extraction: Complete format support for all major RDF serializations.
4//! Extracted and adapted from OxiGraph format libraries with OxiRS enhancements.
5//!
6//! Provides unified parsing and serialization for:
7//! - Turtle (.ttl)
8//! - N-Triples (.nt)
9//! - N-Quads (.nq)
10//! - TriG (.trig)
11//! - RDF/XML (.rdf, .xml)
12//! - JSON-LD (.jsonld)
13//! - N3 (.n3)
14
15pub mod error;
16#[allow(clippy::module_inception)]
17pub mod format;
18pub mod jsonld;
19pub mod n3;
20pub mod n3_lexer;
21pub mod nquads;
22pub mod ntriples;
23pub mod parser;
24pub mod rdfxml;
25pub mod serializer;
26pub mod toolkit;
27pub mod trig;
28pub mod turtle;
29// `turtle_grammar` is an unfinished, dead-code recognizer (all entry points
30// used to silently return empty results instead of parsing). It is kept
31// crate-private and made to fail loudly (see turtle_grammar.rs) rather than
32// being part of the public API; use `format::turtle::TurtleParser` or
33// `crate::parser::Parser` (RdfFormat::Turtle) for real Turtle parsing.
34pub(crate) mod turtle_grammar;
35pub mod w3c_tests;
36
37// Re-export key types
38pub use error::{FormatError, RdfParseError, RdfSyntaxError, TextPosition};
39pub use format::RdfFormat;
40pub use parser::{QuadParseResult, RdfParser, ReaderQuadParser, SliceQuadParser};
41pub use serializer::{QuadSerializeResult, RdfSerializer, WriterQuadSerializer};
42
43// Format-specific re-exports
44pub use format::{JsonLdProfile, JsonLdProfileSet};
45pub use jsonld::{JsonLdParser, JsonLdSerializer};
46pub use n3::N3Serializer;
47pub use nquads::NQuadsSerializer;
48pub use ntriples::{NTriplesParser, NTriplesSerializer};
49pub use rdfxml::{RdfXmlParser, RdfXmlSerializer};
50pub use trig::TriGSerializer;
51pub use turtle::{TurtleParser, TurtleSerializer};
52
53// W3C compliance testing
54pub use w3c_tests::{
55    run_w3c_compliance_tests, RdfComplianceStats, RdfTestResult, RdfTestStatus, RdfTestType,
56    W3cRdfTestConfig, W3cRdfTestSuiteRunner,
57};
58
59use crate::model::{Quad, Triple};
60use crate::OxirsError;
61use std::io::{Read, Write};
62
63/// Result type for format operations
64pub type FormatResult<T> = Result<T, FormatError>;
65
66/// Trait for format detection from content or metadata
67pub trait FormatDetection {
68    /// Detect format from file extension
69    fn from_extension(extension: &str) -> Option<RdfFormat>;
70
71    /// Detect format from media type
72    fn from_media_type(media_type: &str) -> Option<RdfFormat>;
73
74    /// Detect format from content analysis (magic bytes, syntax patterns)
75    fn from_content(content: &[u8]) -> Option<RdfFormat>;
76
77    /// Detect format from filename
78    fn from_filename(filename: &str) -> Option<RdfFormat> {
79        std::path::Path::new(filename)
80            .extension()
81            .and_then(|ext| ext.to_str())
82            .and_then(Self::from_extension)
83    }
84}
85
86/// Unified RDF format handler combining parsing and serialization
87pub struct FormatHandler {
88    format: RdfFormat,
89}
90
91impl FormatHandler {
92    /// Create a new format handler for the specified format
93    pub fn new(format: RdfFormat) -> Self {
94        Self { format }
95    }
96
97    /// Parse RDF from a reader into quads
98    pub fn parse_quads<R: Read + Send + 'static>(&self, reader: R) -> FormatResult<Vec<Quad>> {
99        let parser = RdfParser::new(self.format.clone());
100        let mut quads = Vec::new();
101
102        for quad_result in parser.for_reader(reader) {
103            quads.push(quad_result?);
104        }
105
106        Ok(quads)
107    }
108
109    /// Parse RDF from a reader into triples (only default graph)
110    pub fn parse_triples<R: Read + Send + 'static>(&self, reader: R) -> FormatResult<Vec<Triple>> {
111        let quads = self.parse_quads(reader)?;
112        Ok(quads
113            .into_iter()
114            .filter_map(|quad| quad.triple_in_default_graph())
115            .collect())
116    }
117
118    /// Serialize quads to a writer
119    pub fn serialize_quads<W: Write + 'static>(
120        &self,
121        writer: W,
122        quads: &[Quad],
123    ) -> FormatResult<()> {
124        let mut serializer = RdfSerializer::new(self.format.clone()).for_writer(writer);
125
126        for quad in quads {
127            serializer.serialize_quad(quad.as_ref())?;
128        }
129
130        serializer.finish()?;
131        Ok(())
132    }
133
134    /// Serialize triples to a writer (places in default graph)
135    pub fn serialize_triples<W: Write + 'static>(
136        &self,
137        writer: W,
138        triples: &[Triple],
139    ) -> FormatResult<()> {
140        let quads: Vec<Quad> = triples.iter().map(|triple| triple.clone().into()).collect();
141        self.serialize_quads(writer, &quads)
142    }
143
144    /// Get the format
145    pub fn format(&self) -> RdfFormat {
146        self.format.clone()
147    }
148}
149
150impl FormatDetection for FormatHandler {
151    fn from_extension(extension: &str) -> Option<RdfFormat> {
152        RdfFormat::from_extension(extension)
153    }
154
155    fn from_media_type(media_type: &str) -> Option<RdfFormat> {
156        RdfFormat::from_media_type(media_type)
157    }
158
159    fn from_content(content: &[u8]) -> Option<RdfFormat> {
160        // Simple heuristics for format detection
161        let content_str = std::str::from_utf8(content).ok()?;
162        let content_lower = content_str.to_lowercase();
163
164        // Check for XML-like structures (RDF/XML)
165        if content_lower.contains("<?xml") || content_lower.contains("<rdf:") {
166            return Some(RdfFormat::RdfXml);
167        }
168
169        // Check for JSON-LD
170        if content_lower.trim_start().starts_with('{')
171            && (content_lower.contains("@context") || content_lower.contains("@type"))
172        {
173            return Some(RdfFormat::JsonLd {
174                profile: JsonLdProfileSet::empty(),
175            });
176        }
177
178        // Check for Turtle-family formats
179        if content_lower.contains("@prefix") || content_lower.contains("@base") {
180            if content_lower.contains("graph") {
181                return Some(RdfFormat::TriG);
182            }
183            return Some(RdfFormat::Turtle);
184        }
185
186        // Check for N-Quads (4 terms per line)
187        let lines: Vec<&str> = content_str.lines().take(10).collect();
188        if lines.iter().any(|line| {
189            let parts: Vec<&str> = line.split_whitespace().collect();
190            parts.len() >= 4 && line.ends_with(" .")
191        }) {
192            return Some(RdfFormat::NQuads);
193        }
194
195        // Check for N-Triples (3 terms per line)
196        if lines.iter().any(|line| {
197            let parts: Vec<&str> = line.split_whitespace().collect();
198            parts.len() >= 3 && line.ends_with(" .")
199        }) {
200            return Some(RdfFormat::NTriples);
201        }
202
203        None
204    }
205}
206
207/// Convert OxiRS errors to format errors
208impl From<OxirsError> for FormatError {
209    fn from(err: OxirsError) -> Self {
210        FormatError::InvalidData(err.to_string())
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_format_detection_from_extension() {
220        assert_eq!(
221            FormatHandler::from_extension("ttl"),
222            Some(RdfFormat::Turtle)
223        );
224        assert_eq!(
225            FormatHandler::from_extension("nt"),
226            Some(RdfFormat::NTriples)
227        );
228        assert_eq!(
229            FormatHandler::from_extension("jsonld"),
230            Some(RdfFormat::JsonLd {
231                profile: JsonLdProfileSet::empty()
232            })
233        );
234        assert_eq!(FormatHandler::from_extension("unknown"), None);
235    }
236
237    #[test]
238    fn test_format_detection_from_content() {
239        let turtle_content = b"@prefix ex: <http://example.org/> .\nex:foo ex:bar ex:baz .";
240        assert_eq!(
241            FormatHandler::from_content(turtle_content),
242            Some(RdfFormat::Turtle)
243        );
244
245        let jsonld_content = br#"{"@context": "http://example.org/", "@type": "Person"}"#;
246        assert_eq!(
247            FormatHandler::from_content(jsonld_content),
248            Some(RdfFormat::JsonLd {
249                profile: JsonLdProfileSet::empty()
250            })
251        );
252
253        let rdfxml_content = b"<?xml version=\"1.0\"?>\n<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">";
254        assert_eq!(
255            FormatHandler::from_content(rdfxml_content),
256            Some(RdfFormat::RdfXml)
257        );
258    }
259}