1pub 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;
29pub(crate) mod turtle_grammar;
35pub mod w3c_tests;
36
37pub use error::{FormatError, RdfParseError, RdfSyntaxError, TextPosition};
39pub use format::RdfFormat;
40pub use parser::{QuadParseResult, RdfParser, ReaderQuadParser, SliceQuadParser};
41pub use serializer::{QuadSerializeResult, RdfSerializer, WriterQuadSerializer};
42
43pub 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
53pub 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
63pub type FormatResult<T> = Result<T, FormatError>;
65
66pub trait FormatDetection {
68 fn from_extension(extension: &str) -> Option<RdfFormat>;
70
71 fn from_media_type(media_type: &str) -> Option<RdfFormat>;
73
74 fn from_content(content: &[u8]) -> Option<RdfFormat>;
76
77 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
86pub struct FormatHandler {
88 format: RdfFormat,
89}
90
91impl FormatHandler {
92 pub fn new(format: RdfFormat) -> Self {
94 Self { format }
95 }
96
97 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 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 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 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 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 let content_str = std::str::from_utf8(content).ok()?;
162 let content_lower = content_str.to_lowercase();
163
164 if content_lower.contains("<?xml") || content_lower.contains("<rdf:") {
166 return Some(RdfFormat::RdfXml);
167 }
168
169 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 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 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 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
207impl 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}