Skip to main content

oxirs_core/format/
n3.rs

1//! N3 (Notation3) format serializer and parser
2//!
3//! N3 is a superset of Turtle that adds support for variables, rules, and formulae.
4//! This implementation focuses on the Turtle-compatible subset for now.
5//!
6//! W3C Specification: <https://w3c.github.io/N3/spec/>
7
8use super::error::FormatError;
9use crate::model::{
10    GraphName, Literal, NamedNode, ObjectRef, PredicateRef, Quad, QuadRef, SubjectRef,
11};
12use std::collections::HashMap;
13use std::io::Write;
14
15/// N3 serializer for writing RDF with Turtle-compatible syntax
16#[derive(Debug, Clone)]
17pub struct N3Serializer {
18    /// Base IRI for relative IRI resolution
19    base_iri: Option<String>,
20    /// Prefix declarations for compact serialization
21    prefixes: HashMap<String, String>,
22    /// Pretty printing with indentation
23    pretty: bool,
24}
25
26impl N3Serializer {
27    /// Create a new N3 serializer
28    pub fn new() -> Self {
29        let mut prefixes = HashMap::new();
30
31        // Add standard N3 prefixes
32        prefixes.insert(
33            "rdf".to_string(),
34            "http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
35        );
36        prefixes.insert(
37            "rdfs".to_string(),
38            "http://www.w3.org/2000/01/rdf-schema#".to_string(),
39        );
40        prefixes.insert(
41            "xsd".to_string(),
42            "http://www.w3.org/2001/XMLSchema#".to_string(),
43        );
44        prefixes.insert(
45            "owl".to_string(),
46            "http://www.w3.org/2002/07/owl#".to_string(),
47        );
48
49        Self {
50            base_iri: None,
51            prefixes,
52            pretty: false,
53        }
54    }
55
56    /// Set the base IRI
57    pub fn with_base_iri(mut self, base: &str) -> Self {
58        self.base_iri = Some(base.to_string());
59        self
60    }
61
62    /// Add a prefix mapping
63    pub fn with_prefix(mut self, prefix: &str, iri: &str) -> Self {
64        self.prefixes.insert(prefix.to_string(), iri.to_string());
65        self
66    }
67
68    /// Enable pretty printing
69    pub fn pretty(mut self) -> Self {
70        self.pretty = true;
71        self
72    }
73
74    /// Wrap this serializer for a specific writer
75    pub fn for_writer<W: Write + 'static>(self, writer: W) -> N3Writer<W> {
76        N3Writer {
77            writer,
78            serializer: self,
79            buffer: Vec::new(),
80        }
81    }
82
83    /// Serialize quads as N3 triples (only default graph)
84    fn serialize_quads<W: Write>(&self, quads: &[Quad], writer: &mut W) -> Result<(), FormatError> {
85        // Write prefix declarations
86        for (prefix, namespace) in &self.prefixes {
87            writeln!(writer, "@prefix {}: <{}> .", prefix, namespace).map_err(FormatError::from)?;
88        }
89
90        if !self.prefixes.is_empty() {
91            writeln!(writer).map_err(FormatError::from)?;
92        }
93
94        // Write base if present
95        if let Some(base) = &self.base_iri {
96            writeln!(writer, "@base <{}> .", base).map_err(FormatError::from)?;
97            writeln!(writer).map_err(FormatError::from)?;
98        }
99
100        // Serialize triples (only from default graph)
101        for quad in quads {
102            // N3 typically handles only default graph; named graphs would need special syntax
103            if matches!(quad.graph_name(), GraphName::DefaultGraph) {
104                self.serialize_triple(quad.as_ref(), writer)?;
105                writeln!(writer, " .").map_err(FormatError::from)?;
106            }
107        }
108
109        Ok(())
110    }
111
112    fn serialize_triple<W: Write>(
113        &self,
114        quad: QuadRef<'_>,
115        writer: &mut W,
116    ) -> Result<(), FormatError> {
117        self.write_subject(quad.subject(), writer)?;
118        write!(writer, " ").map_err(FormatError::from)?;
119
120        self.write_predicate(quad.predicate(), writer)?;
121        write!(writer, " ").map_err(FormatError::from)?;
122
123        self.write_object(quad.object(), writer)?;
124
125        Ok(())
126    }
127
128    fn write_subject<W: Write>(
129        &self,
130        subject: SubjectRef<'_>,
131        writer: &mut W,
132    ) -> Result<(), FormatError> {
133        match subject {
134            SubjectRef::NamedNode(node) => self.write_named_node(node, writer)?,
135            SubjectRef::BlankNode(node) => {
136                let id = node.as_str();
137                let id = id.strip_prefix("_:").unwrap_or(id);
138                write!(writer, "_:{}", id).map_err(FormatError::from)?;
139            }
140            SubjectRef::Variable(var) => {
141                // N3 supports variables with ?variable syntax
142                write!(writer, "?{}", var.name()).map_err(FormatError::from)?;
143            }
144            SubjectRef::QuotedTriple(qt) => self.write_quoted_triple(qt.inner(), writer)?,
145        }
146        Ok(())
147    }
148
149    fn write_quoted_triple<W: Write>(
150        &self,
151        inner: &crate::model::Triple,
152        writer: &mut W,
153    ) -> Result<(), FormatError> {
154        write!(writer, "<< ").map_err(FormatError::from)?;
155        self.write_subject(inner.subject().into(), writer)?;
156        write!(writer, " ").map_err(FormatError::from)?;
157        self.write_predicate(inner.predicate().into(), writer)?;
158        write!(writer, " ").map_err(FormatError::from)?;
159        self.write_object(inner.object().into(), writer)?;
160        write!(writer, " >>").map_err(FormatError::from)?;
161        Ok(())
162    }
163
164    fn write_predicate<W: Write>(
165        &self,
166        predicate: PredicateRef<'_>,
167        writer: &mut W,
168    ) -> Result<(), FormatError> {
169        match predicate {
170            PredicateRef::NamedNode(node) => {
171                // N3 uses 'a' for rdf:type
172                if node.as_str() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" {
173                    write!(writer, "a").map_err(FormatError::from)?;
174                } else if node.as_str() == "http://www.w3.org/2002/07/owl#sameAs" {
175                    // N3 uses '=' for owl:sameAs
176                    write!(writer, "=").map_err(FormatError::from)?;
177                } else {
178                    self.write_named_node(node, writer)?;
179                }
180            }
181            PredicateRef::Variable(var) => {
182                write!(writer, "?{}", var.name()).map_err(FormatError::from)?;
183            }
184        }
185        Ok(())
186    }
187
188    fn write_object<W: Write>(
189        &self,
190        object: ObjectRef<'_>,
191        writer: &mut W,
192    ) -> Result<(), FormatError> {
193        match object {
194            ObjectRef::NamedNode(node) => self.write_named_node(node, writer)?,
195            ObjectRef::BlankNode(node) => {
196                let id = node.as_str();
197                let id = id.strip_prefix("_:").unwrap_or(id);
198                write!(writer, "_:{}", id).map_err(FormatError::from)?;
199            }
200            ObjectRef::Literal(literal) => self.write_literal(literal, writer)?,
201            ObjectRef::Variable(var) => {
202                write!(writer, "?{}", var.name()).map_err(FormatError::from)?;
203            }
204            ObjectRef::QuotedTriple(qt) => self.write_quoted_triple(qt.inner(), writer)?,
205        }
206        Ok(())
207    }
208
209    fn write_named_node<W: Write>(
210        &self,
211        node: &NamedNode,
212        writer: &mut W,
213    ) -> Result<(), FormatError> {
214        let iri = node.as_str();
215
216        // Try to use a prefix
217        for (prefix, namespace) in &self.prefixes {
218            if let Some(local) = iri.strip_prefix(namespace) {
219                write!(writer, "{}:{}", prefix, local).map_err(FormatError::from)?;
220                return Ok(());
221            }
222        }
223
224        // Use full IRI
225        write!(writer, "<{}>", iri).map_err(FormatError::from)?;
226        Ok(())
227    }
228
229    fn write_literal<W: Write>(
230        &self,
231        literal: &Literal,
232        writer: &mut W,
233    ) -> Result<(), FormatError> {
234        let value = literal.value();
235
236        // N3 supports some numeric shortcuts
237        if let Some(_datatype) = self.check_numeric_shortcut(literal) {
238            write!(writer, "{}", value).map_err(FormatError::from)?;
239            return Ok(());
240        }
241
242        // Regular string literal
243        let escaped = self.escape_string(value);
244        write!(writer, "\"{}\"", escaped).map_err(FormatError::from)?;
245
246        if let Some(lang) = literal.language() {
247            write!(writer, "@{}", lang).map_err(FormatError::from)?;
248        } else {
249            let datatype = literal.datatype();
250            if datatype.as_str() != "http://www.w3.org/2001/XMLSchema#string" {
251                write!(writer, "^^").map_err(FormatError::from)?;
252                self.write_named_node(&datatype.into_owned(), writer)?;
253            }
254        }
255
256        Ok(())
257    }
258
259    fn check_numeric_shortcut(&self, literal: &Literal) -> Option<String> {
260        let datatype = literal.datatype();
261        let value = literal.value();
262
263        match datatype.as_str() {
264            "http://www.w3.org/2001/XMLSchema#integer" if value.parse::<i64>().is_ok() => {
265                Some("integer".to_string())
266            }
267            "http://www.w3.org/2001/XMLSchema#decimal" if value.parse::<f64>().is_ok() => {
268                Some("decimal".to_string())
269            }
270            "http://www.w3.org/2001/XMLSchema#double" if value.parse::<f64>().is_ok() => {
271                Some("double".to_string())
272            }
273            "http://www.w3.org/2001/XMLSchema#boolean" if value == "true" || value == "false" => {
274                Some("boolean".to_string())
275            }
276            _ => None,
277        }
278    }
279
280    fn escape_string(&self, s: &str) -> String {
281        let mut result = String::with_capacity(s.len());
282        for ch in s.chars() {
283            match ch {
284                '\\' => result.push_str("\\\\"),
285                '\"' => result.push_str("\\\""),
286                '\n' => result.push_str("\\n"),
287                '\r' => result.push_str("\\r"),
288                '\t' => result.push_str("\\t"),
289                c if c.is_control() => {
290                    result.push_str(&format!("\\u{:04X}", c as u32));
291                }
292                c => result.push(c),
293            }
294        }
295        result
296    }
297}
298
299impl Default for N3Serializer {
300    fn default() -> Self {
301        Self::new()
302    }
303}
304
305/// Writer wrapper for N3 serialization
306pub struct N3Writer<W: Write> {
307    writer: W,
308    serializer: N3Serializer,
309    buffer: Vec<Quad>,
310}
311
312impl<W: Write> N3Writer<W> {
313    /// Serialize a single quad (buffered until finish)
314    pub fn serialize_quad(&mut self, quad: QuadRef<'_>) -> Result<(), FormatError> {
315        self.buffer.push(quad.into());
316        Ok(())
317    }
318
319    /// Finish serialization and return the writer
320    pub fn finish(mut self) -> Result<W, FormatError> {
321        self.serializer
322            .serialize_quads(&self.buffer, &mut self.writer)?;
323        Ok(self.writer)
324    }
325}
326
327/// Implement the QuadSerializer trait for integration with the format system
328impl<W: Write> super::serializer::QuadSerializer<W> for N3Writer<W> {
329    fn serialize_quad(&mut self, quad: QuadRef<'_>) -> super::serializer::QuadSerializeResult {
330        N3Writer::serialize_quad(self, quad)
331            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
332    }
333
334    fn finish(self: Box<Self>) -> super::error::SerializeResult<W> {
335        N3Writer::finish(*self).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::model::{NamedNode, Object, Subject, Triple};
343
344    #[test]
345    fn test_n3_serialize_triple() {
346        let serializer = N3Serializer::new();
347        let mut writer = Vec::new();
348
349        let triple = Triple::new(
350            Subject::NamedNode(NamedNode::new("http://example.org/subject").expect("valid IRI")),
351            NamedNode::new("http://example.org/predicate").expect("valid IRI"),
352            Object::NamedNode(NamedNode::new("http://example.org/object").expect("valid IRI")),
353        );
354
355        let quads = vec![Quad::from(triple)];
356        serializer
357            .serialize_quads(&quads, &mut writer)
358            .expect("operation should succeed");
359
360        let output = String::from_utf8(writer).expect("bytes should be valid UTF-8");
361        assert!(output.contains("@prefix"));
362        assert!(output.contains("<http://example.org/subject>"));
363        assert!(output.contains("<http://example.org/predicate>"));
364        assert!(output.contains("<http://example.org/object>"));
365    }
366
367    #[test]
368    fn test_n3_rdf_type_abbreviation() {
369        let serializer = N3Serializer::new();
370        let mut writer = Vec::new();
371
372        let triple = Triple::new(
373            Subject::NamedNode(NamedNode::new("http://example.org/subject").expect("valid IRI")),
374            NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type").expect("valid IRI"),
375            Object::NamedNode(NamedNode::new("http://example.org/Type").expect("valid IRI")),
376        );
377
378        let quads = vec![Quad::from(triple)];
379        serializer
380            .serialize_quads(&quads, &mut writer)
381            .expect("operation should succeed");
382
383        let output = String::from_utf8(writer).expect("bytes should be valid UTF-8");
384        assert!(output.contains(" a "));
385    }
386}