Skip to main content

oxirs_core/
serializer.rs

1//! RDF serialization utilities for various formats
2//!
3//! **Stability**: ✅ **Stable** - Core serializer APIs are production-ready.
4//!
5//! This module provides serializers to export RDF data to various standard formats.
6//! It supports both triple-based (graph) and quad-based (dataset) serialization.
7//!
8//! ## Supported Formats
9//!
10//! ### Triple Formats (Graphs)
11//! - **Turtle** (.ttl) - Compact, human-readable with prefixes
12//! - **N-Triples** (.nt) - Line-based, simple format
13//! - **RDF/XML** (.rdf) - XML-based format
14//! - **JSON-LD** (.jsonld) - JSON format for linked data
15//!
16//! ### Quad Formats (Datasets)
17//! - **TriG** (.trig) - Turtle with named graph support
18//! - **N-Quads** (.nq) - Line-based quad format
19//!
20//! ## Examples
21//!
22//! ### Basic Serialization
23//!
24//! ```rust
25//! use oxirs_core::serializer::Serializer;
26//! use oxirs_core::parser::RdfFormat;
27//! use oxirs_core::model::{Graph, NamedNode, Triple, Literal};
28//!
29//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
30//! // Create a graph with some data
31//! let mut graph = Graph::new();
32//! let triple = Triple::new(
33//!     NamedNode::new("http://example.org/alice")?,
34//!     NamedNode::new("http://xmlns.com/foaf/0.1/name")?,
35//!     Literal::new("Alice"),
36//! );
37//! graph.insert(triple);
38//!
39//! // Serialize to Turtle format
40//! let serializer = Serializer::new(RdfFormat::Turtle);
41//! let turtle = serializer.serialize_graph(&graph)?;
42//! println!("{}", turtle);
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! ### Serialize to N-Triples
48//!
49//! ```rust
50//! use oxirs_core::serializer::Serializer;
51//! use oxirs_core::parser::RdfFormat;
52//! use oxirs_core::model::{Graph, NamedNode, Triple};
53//!
54//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
55//! # let mut graph = Graph::new();
56//! # let triple = Triple::new(
57//! #     NamedNode::new("http://example.org/alice")?,
58//! #     NamedNode::new("http://xmlns.com/foaf/0.1/knows")?,
59//! #     NamedNode::new("http://example.org/bob")?,
60//! # );
61//! # graph.insert(triple);
62//! // N-Triples: one triple per line, no prefixes
63//! let serializer = Serializer::new(RdfFormat::NTriples);
64//! let ntriples = serializer.serialize_graph(&graph)?;
65//!
66//! // Output:
67//! // <http://example.org/alice> <http://xmlns.com/foaf/0.1/knows> <http://example.org/bob> .
68//! # Ok(())
69//! # }
70//! ```
71//!
72//! ### Serialize Datasets with Named Graphs
73//!
74//! ```rust
75//! use oxirs_core::serializer::Serializer;
76//! use oxirs_core::parser::RdfFormat;
77//! use oxirs_core::model::{Dataset, NamedNode, Quad, GraphName, Literal};
78//!
79//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
80//! // Create a dataset with named graphs
81//! let mut dataset = Dataset::new();
82//!
83//! let graph1 = GraphName::NamedNode(NamedNode::new("http://example.org/graph1")?);
84//! let quad1 = Quad::new(
85//!     NamedNode::new("http://example.org/alice")?,
86//!     NamedNode::new("http://xmlns.com/foaf/0.1/name")?,
87//!     Literal::new("Alice"),
88//!     graph1,
89//! );
90//! dataset.insert(quad1);
91//!
92//! // Serialize to N-Quads format
93//! let serializer = Serializer::new(RdfFormat::NQuads);
94//! let nquads = serializer.serialize_dataset(&dataset)?;
95//! println!("{}", nquads);
96//! # Ok(())
97//! # }
98//! ```
99//!
100//! ### Serialize to TriG (Turtle with Graphs)
101//!
102//! ```rust
103//! use oxirs_core::serializer::Serializer;
104//! use oxirs_core::parser::RdfFormat;
105//! use oxirs_core::model::{Dataset, NamedNode, Quad, GraphName, Literal};
106//!
107//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
108//! # let mut dataset = Dataset::new();
109//! # let graph = GraphName::NamedNode(NamedNode::new("http://example.org/graph1")?);
110//! # dataset.insert(Quad::new(
111//! #     NamedNode::new("http://example.org/alice")?,
112//! #     NamedNode::new("http://xmlns.com/foaf/0.1/name")?,
113//! #     Literal::new("Alice"),
114//! #     graph,
115//! # ));
116//! // TriG: Turtle syntax with named graph blocks
117//! let serializer = Serializer::new(RdfFormat::TriG);
118//! let trig = serializer.serialize_dataset(&dataset)?;
119//!
120//! // Output includes graph blocks:
121//! // <http://example.org/graph1> {
122//! //     <http://example.org/alice> <http://xmlns.com/foaf/0.1/name> "Alice" .
123//! // }
124//! # Ok(())
125//! # }
126//! ```
127//!
128//! ### Writing to Files
129//!
130//! ```rust,no_run
131//! use oxirs_core::serializer::Serializer;
132//! use oxirs_core::parser::RdfFormat;
133//! use oxirs_core::model::Graph;
134//! use std::fs;
135//!
136//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
137//! # let graph = Graph::new();
138//! // Serialize and write to file
139//! let serializer = Serializer::new(RdfFormat::Turtle);
140//! let turtle = serializer.serialize_graph(&graph)?;
141//! fs::write("output.ttl", turtle)?;
142//! # Ok(())
143//! # }
144//! ```
145//!
146//! ### Quad Serialization Helper
147//!
148//! ```rust
149//! use oxirs_core::serializer::Serializer;
150//! use oxirs_core::parser::RdfFormat;
151//! use oxirs_core::model::{NamedNode, Quad, Literal, GraphName};
152//!
153//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
154//! let quad = Quad::new(
155//!     NamedNode::new("http://example.org/subject")?,
156//!     NamedNode::new("http://example.org/predicate")?,
157//!     Literal::new("value"),
158//!     GraphName::DefaultGraph,
159//! );
160//!
161//! let serializer = Serializer::new(RdfFormat::NQuads);
162//! let nquad_line = serializer.serialize_quad_to_nquads(&quad)?;
163//! println!("{}", nquad_line);
164//! # Ok(())
165//! # }
166//! ```
167//!
168//! ## Format Capabilities
169//!
170//! | Format | Graphs | Quads | Prefixes | Compact | Human-Readable |
171//! |--------|--------|-------|----------|---------|----------------|
172//! | Turtle | ✅ | ❌ | ✅ | ✅ | ✅ |
173//! | N-Triples | ✅ | ❌ | ❌ | ❌ | ⚠️ |
174//! | TriG | ✅ | ✅ | ✅ | ✅ | ✅ |
175//! | N-Quads | ✅ | ✅ | ❌ | ❌ | ⚠️ |
176//! | RDF/XML | ✅ | ❌ | ✅ | ❌ | ⚠️ |
177//! | JSON-LD | ✅ | ✅ | ✅ | ⚠️ | ⚠️ |
178//!
179//! ## Format Selection Guidelines
180//!
181//! - **For human readability**: Use Turtle or TriG
182//! - **For machine processing**: Use N-Triples or N-Quads
183//! - **For web APIs**: Use JSON-LD
184//! - **For XML systems**: Use RDF/XML
185//! - **For named graphs**: Use TriG or N-Quads
186//!
187//! ## Performance Tips
188//!
189//! 1. **N-Triples/N-Quads** - Fastest to serialize (line-based)
190//! 2. **Large datasets** - Use N-Quads for streaming serialization
191//! 3. **Prefixes** - Turtle/TriG reduce output size with prefix declarations
192//! 4. **Human review** - Turtle/TriG are most readable
193//!
194//! ## Error Handling
195//!
196//! Serialization can fail in these cases:
197//! - **Format mismatch** - Using quad-only format for graphs (e.g., TriG for a Graph)
198//! - **Unsupported features** - Variables in concrete formats
199//! - **Invalid data** - Malformed IRIs or literals
200//!
201//! ## Related Modules
202//!
203//! - [`crate::parser`] - Parse RDF from various formats
204//! - [`crate::model`] - RDF data model types
205//! - [`crate::rdf_store`] - Store RDF data
206
207use crate::{
208    model::{dataset::Dataset, graph::Graph, GraphName, Quad},
209    parser::RdfFormat,
210    Result,
211};
212
213/// RDF serializer interface
214pub struct Serializer {
215    format: RdfFormat,
216}
217
218impl Serializer {
219    /// Create a new serializer for the specified format
220    pub fn new(format: RdfFormat) -> Self {
221        Serializer { format }
222    }
223
224    /// Serialize a graph to a string
225    pub fn serialize_graph(&self, graph: &Graph) -> Result<String> {
226        match self.format {
227            RdfFormat::Turtle => self.serialize_turtle(graph),
228            RdfFormat::NTriples => self.serialize_ntriples(graph),
229            RdfFormat::TriG => self.serialize_trig_graph(graph),
230            RdfFormat::NQuads => self.serialize_nquads_graph(graph),
231            RdfFormat::RdfXml => self.serialize_rdfxml(graph),
232            RdfFormat::JsonLd => self.serialize_jsonld(graph),
233        }
234    }
235
236    /// Serialize a dataset to a string (supports quad-based formats)
237    pub fn serialize_dataset(&self, dataset: &Dataset) -> Result<String> {
238        match self.format {
239            RdfFormat::Turtle => Err(crate::OxirsError::Serialize(
240                "Turtle format does not support datasets (use TriG instead)".to_string(),
241            )),
242            RdfFormat::NTriples => Err(crate::OxirsError::Serialize(
243                "N-Triples format does not support datasets (use N-Quads instead)".to_string(),
244            )),
245            RdfFormat::TriG => self.serialize_trig_dataset(dataset),
246            RdfFormat::NQuads => self.serialize_nquads_dataset(dataset),
247            RdfFormat::RdfXml => Err(crate::OxirsError::Serialize(
248                "RDF/XML dataset serialization not yet implemented".to_string(),
249            )),
250            RdfFormat::JsonLd => Err(crate::OxirsError::Serialize(
251                "JSON-LD dataset serialization not yet implemented".to_string(),
252            )),
253        }
254    }
255
256    /// Legacy method for backward compatibility
257    pub fn serialize(&self, graph: &Graph) -> Result<String> {
258        self.serialize_graph(graph)
259    }
260
261    fn serialize_turtle(&self, graph: &Graph) -> Result<String> {
262        let mut serializer = TurtleSerializer::new();
263        serializer.serialize_graph(graph)
264    }
265
266    fn serialize_ntriples(&self, graph: &Graph) -> Result<String> {
267        let mut result = String::new();
268
269        for triple in graph.iter() {
270            // Serialize subject
271            match triple.subject() {
272                crate::model::Subject::NamedNode(node) => {
273                    result.push_str(&format!("<{}>", node.as_str()));
274                }
275                crate::model::Subject::BlankNode(node) => {
276                    result.push_str(&format!("{node}"));
277                }
278                crate::model::Subject::Variable(_) => {
279                    return Err(crate::OxirsError::Serialize(
280                        "Variables not supported in N-Triples serialization".to_string(),
281                    ));
282                }
283                crate::model::Subject::QuotedTriple(_) => {
284                    return Err(crate::OxirsError::Serialize(
285                        "Quoted triples not supported in N-Triples serialization".to_string(),
286                    ));
287                }
288            }
289
290            result.push(' ');
291
292            // Serialize predicate
293            match triple.predicate() {
294                crate::model::Predicate::NamedNode(node) => {
295                    result.push_str(&format!("<{}>", node.as_str()));
296                }
297                crate::model::Predicate::Variable(_) => {
298                    return Err(crate::OxirsError::Serialize(
299                        "Variables not supported in N-Triples serialization".to_string(),
300                    ));
301                }
302            }
303
304            result.push(' ');
305
306            // Serialize object
307            match triple.object() {
308                crate::model::Object::NamedNode(node) => {
309                    result.push_str(&format!("<{}>", node.as_str()));
310                }
311                crate::model::Object::BlankNode(node) => {
312                    result.push_str(&format!("{node}"));
313                }
314                crate::model::Object::Literal(literal) => {
315                    result.push('"');
316                    // Escape quotes and backslashes in literal value
317                    for c in literal.value().chars() {
318                        match c {
319                            '"' => result.push_str("\\\""),
320                            '\\' => result.push_str("\\\\"),
321                            '\n' => result.push_str("\\n"),
322                            '\r' => result.push_str("\\r"),
323                            '\t' => result.push_str("\\t"),
324                            _ => result.push(c),
325                        }
326                    }
327                    result.push('"');
328
329                    // Add language tag or datatype
330                    if let Some(lang) = literal.language() {
331                        result.push_str(&format!("@{lang}"));
332                    } else {
333                        let datatype = literal.datatype();
334                        if datatype.as_str() != "http://www.w3.org/2001/XMLSchema#string" {
335                            result.push_str(&format!("^^<{}>", datatype.as_str()));
336                        }
337                    }
338                }
339                crate::model::Object::Variable(_) => {
340                    return Err(crate::OxirsError::Serialize(
341                        "Variables not supported in N-Triples serialization".to_string(),
342                    ));
343                }
344                crate::model::Object::QuotedTriple(_) => {
345                    return Err(crate::OxirsError::Serialize(
346                        "Quoted triples not supported in N-Triples serialization".to_string(),
347                    ));
348                }
349            }
350
351            result.push_str(" .\n");
352        }
353
354        Ok(result)
355    }
356
357    fn serialize_rdfxml(&self, graph: &Graph) -> Result<String> {
358        use oxrdfxml::RdfXmlSerializer;
359
360        let mut output = Vec::new();
361        let mut serializer = RdfXmlSerializer::new().for_writer(&mut output);
362
363        // Convert OxiRS triples to oxrdf triples and serialize
364        for triple in graph.iter() {
365            // Convert to oxrdf::Triple
366            let oxrdf_triple = self.convert_triple_to_oxrdf(triple)?;
367            serializer.serialize_triple(&oxrdf_triple).map_err(|e| {
368                crate::OxirsError::Serialize(format!("RDF/XML serialization error: {e}"))
369            })?;
370        }
371
372        serializer
373            .finish()
374            .map_err(|e| crate::OxirsError::Serialize(format!("RDF/XML finish error: {e}")))?;
375
376        String::from_utf8(output)
377            .map_err(|e| crate::OxirsError::Serialize(format!("UTF-8 conversion error: {e}")))
378    }
379
380    fn serialize_trig_graph(&self, graph: &Graph) -> Result<String> {
381        use oxttl::TriGSerializer;
382
383        let mut output = Vec::new();
384        let mut serializer = TriGSerializer::new().for_writer(&mut output);
385
386        // Serialize all triples as quads in the default graph
387        for triple in graph.iter() {
388            let oxrdf_quad = self.convert_triple_to_oxrdf_quad(triple, None)?;
389            serializer.serialize_quad(&oxrdf_quad).map_err(|e| {
390                crate::OxirsError::Serialize(format!("TriG serialization error: {e}"))
391            })?;
392        }
393
394        serializer
395            .finish()
396            .map_err(|e| crate::OxirsError::Serialize(format!("TriG finish error: {e}")))?;
397
398        String::from_utf8(output)
399            .map_err(|e| crate::OxirsError::Serialize(format!("UTF-8 conversion error: {e}")))
400    }
401
402    fn serialize_trig_dataset(&self, dataset: &Dataset) -> Result<String> {
403        use oxttl::TriGSerializer;
404
405        let mut output = Vec::new();
406        let mut serializer = TriGSerializer::new().for_writer(&mut output);
407
408        // Serialize all quads
409        for quad in dataset.iter() {
410            let oxrdf_quad = self.convert_quad_to_oxrdf(&quad)?;
411            serializer.serialize_quad(&oxrdf_quad).map_err(|e| {
412                crate::OxirsError::Serialize(format!("TriG serialization error: {e}"))
413            })?;
414        }
415
416        serializer
417            .finish()
418            .map_err(|e| crate::OxirsError::Serialize(format!("TriG finish error: {e}")))?;
419
420        String::from_utf8(output)
421            .map_err(|e| crate::OxirsError::Serialize(format!("UTF-8 conversion error: {e}")))
422    }
423
424    fn serialize_nquads_graph(&self, graph: &Graph) -> Result<String> {
425        // For a graph, serialize all triples as quads in the default graph
426        let mut result = String::new();
427
428        for triple in graph.iter() {
429            result.push_str(&self.serialize_quad_to_nquads(&Quad::from_triple(triple.clone()))?);
430        }
431
432        Ok(result)
433    }
434
435    fn serialize_nquads_dataset(&self, dataset: &Dataset) -> Result<String> {
436        let mut result = String::new();
437
438        for quad in dataset.iter() {
439            result.push_str(&self.serialize_quad_to_nquads(&quad)?);
440        }
441
442        Ok(result)
443    }
444
445    pub fn serialize_quad_to_nquads(&self, quad: &Quad) -> Result<String> {
446        let mut result = String::new();
447
448        // Serialize subject
449        match quad.subject() {
450            crate::model::Subject::NamedNode(node) => {
451                result.push_str(&format!("<{}>", node.as_str()));
452            }
453            crate::model::Subject::BlankNode(node) => {
454                result.push_str(&format!("{node}"));
455            }
456            crate::model::Subject::Variable(_) => {
457                return Err(crate::OxirsError::Serialize(
458                    "Variables not supported in N-Quads serialization".to_string(),
459                ));
460            }
461            crate::model::Subject::QuotedTriple(_) => {
462                return Err(crate::OxirsError::Serialize(
463                    "Quoted triples not supported in N-Quads serialization".to_string(),
464                ));
465            }
466        }
467
468        result.push(' ');
469
470        // Serialize predicate
471        match quad.predicate() {
472            crate::model::Predicate::NamedNode(node) => {
473                result.push_str(&format!("<{}>", node.as_str()));
474            }
475            crate::model::Predicate::Variable(_) => {
476                return Err(crate::OxirsError::Serialize(
477                    "Variables not supported in N-Quads serialization".to_string(),
478                ));
479            }
480        }
481
482        result.push(' ');
483
484        // Serialize object
485        match quad.object() {
486            crate::model::Object::NamedNode(node) => {
487                result.push_str(&format!("<{}>", node.as_str()));
488            }
489            crate::model::Object::BlankNode(node) => {
490                result.push_str(&format!("{node}"));
491            }
492            crate::model::Object::Literal(literal) => {
493                result.push('"');
494                // Escape quotes and backslashes in literal value
495                for c in literal.value().chars() {
496                    match c {
497                        '"' => result.push_str("\\\""),
498                        '\\' => result.push_str("\\\\"),
499                        '\n' => result.push_str("\\n"),
500                        '\r' => result.push_str("\\r"),
501                        '\t' => result.push_str("\\t"),
502                        _ => result.push(c),
503                    }
504                }
505                result.push('"');
506
507                // Add language tag or datatype
508                if let Some(lang) = literal.language() {
509                    result.push_str(&format!("@{lang}"));
510                } else {
511                    let datatype = literal.datatype();
512                    if datatype.as_str() != "http://www.w3.org/2001/XMLSchema#string" {
513                        result.push_str(&format!("^^<{}>", datatype.as_str()));
514                    }
515                }
516            }
517            crate::model::Object::Variable(_) => {
518                return Err(crate::OxirsError::Serialize(
519                    "Variables not supported in N-Quads serialization".to_string(),
520                ));
521            }
522            crate::model::Object::QuotedTriple(_) => {
523                return Err(crate::OxirsError::Serialize(
524                    "Quoted triples not supported in N-Quads serialization".to_string(),
525                ));
526            }
527        }
528
529        result.push(' ');
530
531        // Serialize graph name
532        match quad.graph_name() {
533            GraphName::NamedNode(node) => {
534                result.push_str(&format!("<{}>", node.as_str()));
535            }
536            GraphName::BlankNode(node) => {
537                result.push_str(&format!("{node}"));
538            }
539            GraphName::Variable(_) => {
540                return Err(crate::OxirsError::Serialize(
541                    "Variables not supported in N-Quads serialization".to_string(),
542                ));
543            }
544            GraphName::DefaultGraph => {
545                // For default graph, we can either omit the graph name entirely
546                // or use a special representation. Let's omit it to make it N-Triples compatible
547                result.pop(); // Remove the trailing space
548                result.push_str(" .\n");
549                return Ok(result);
550            }
551        }
552
553        result.push_str(" .\n");
554
555        Ok(result)
556    }
557
558    fn serialize_jsonld(&self, graph: &Graph) -> Result<String> {
559        use oxjsonld::JsonLdSerializer;
560
561        let mut output = Vec::new();
562        let mut serializer = JsonLdSerializer::new().for_writer(&mut output);
563
564        // Convert OxiRS triples to oxrdf triples and serialize
565        for triple in graph.iter() {
566            let oxrdf_quad = self.convert_triple_to_oxrdf_quad(triple, None)?;
567            serializer.serialize_quad(&oxrdf_quad).map_err(|e| {
568                crate::OxirsError::Serialize(format!("JSON-LD serialization error: {e}"))
569            })?;
570        }
571
572        serializer
573            .finish()
574            .map_err(|e| crate::OxirsError::Serialize(format!("JSON-LD finish error: {e}")))?;
575
576        String::from_utf8(output)
577            .map_err(|e| crate::OxirsError::Serialize(format!("UTF-8 conversion error: {e}")))
578    }
579
580    /// Convert OxiRS Triple to oxrdf::Triple for use with external serializers
581    fn convert_triple_to_oxrdf(&self, triple: &crate::model::Triple) -> Result<oxrdf::Triple> {
582        // Convert subject
583        let subject = match triple.subject() {
584            crate::model::Subject::NamedNode(n) => {
585                oxrdf::NamedOrBlankNode::NamedNode(oxrdf::NamedNode::new_unchecked(n.as_str()))
586            }
587            crate::model::Subject::BlankNode(b) => {
588                oxrdf::NamedOrBlankNode::BlankNode(oxrdf::BlankNode::new_unchecked(b.as_str()))
589            }
590            _ => {
591                return Err(crate::OxirsError::Serialize(
592                    "Variables and quoted triples not supported in serialization".to_string(),
593                ))
594            }
595        };
596
597        // Convert predicate
598        let predicate = match triple.predicate() {
599            crate::model::Predicate::NamedNode(n) => oxrdf::NamedNode::new_unchecked(n.as_str()),
600            _ => {
601                return Err(crate::OxirsError::Serialize(
602                    "Variable predicates not supported in serialization".to_string(),
603                ))
604            }
605        };
606
607        // Convert object
608        let object = match triple.object() {
609            crate::model::Object::NamedNode(n) => {
610                oxrdf::Term::NamedNode(oxrdf::NamedNode::new_unchecked(n.as_str()))
611            }
612            crate::model::Object::BlankNode(b) => {
613                oxrdf::Term::BlankNode(oxrdf::BlankNode::new_unchecked(b.as_str()))
614            }
615            crate::model::Object::Literal(l) => {
616                let literal = if let Some(lang) = l.language() {
617                    oxrdf::Literal::new_language_tagged_literal_unchecked(l.value(), lang)
618                } else {
619                    let datatype = oxrdf::NamedNode::new_unchecked(l.datatype().as_str());
620                    oxrdf::Literal::new_typed_literal(l.value(), datatype)
621                };
622                oxrdf::Term::Literal(literal)
623            }
624            _ => {
625                return Err(crate::OxirsError::Serialize(
626                    "Variable objects and quoted triples not supported in serialization"
627                        .to_string(),
628                ))
629            }
630        };
631
632        Ok(oxrdf::Triple::new(subject, predicate, object))
633    }
634
635    /// Convert OxiRS Triple to oxrdf::Quad (with optional graph name)
636    fn convert_triple_to_oxrdf_quad(
637        &self,
638        triple: &crate::model::Triple,
639        graph_name: Option<&oxrdf::GraphName>,
640    ) -> Result<oxrdf::Quad> {
641        let oxrdf_triple = self.convert_triple_to_oxrdf(triple)?;
642        let graph = graph_name
643            .cloned()
644            .unwrap_or(oxrdf::GraphName::DefaultGraph);
645        Ok(oxrdf::Quad::new(
646            oxrdf_triple.subject,
647            oxrdf_triple.predicate,
648            oxrdf_triple.object,
649            graph,
650        ))
651    }
652
653    /// Convert OxiRS Quad to oxrdf::Quad
654    fn convert_quad_to_oxrdf(&self, quad: &Quad) -> Result<oxrdf::Quad> {
655        // Convert subject
656        let subject = match quad.subject() {
657            crate::model::Subject::NamedNode(n) => {
658                oxrdf::NamedOrBlankNode::NamedNode(oxrdf::NamedNode::new_unchecked(n.as_str()))
659            }
660            crate::model::Subject::BlankNode(b) => {
661                oxrdf::NamedOrBlankNode::BlankNode(oxrdf::BlankNode::new_unchecked(b.as_str()))
662            }
663            _ => {
664                return Err(crate::OxirsError::Serialize(
665                    "Variables and quoted triples not supported in serialization".to_string(),
666                ))
667            }
668        };
669
670        // Convert predicate
671        let predicate = match quad.predicate() {
672            crate::model::Predicate::NamedNode(n) => oxrdf::NamedNode::new_unchecked(n.as_str()),
673            _ => {
674                return Err(crate::OxirsError::Serialize(
675                    "Variable predicates not supported in serialization".to_string(),
676                ))
677            }
678        };
679
680        // Convert object
681        let object = match quad.object() {
682            crate::model::Object::NamedNode(n) => {
683                oxrdf::Term::NamedNode(oxrdf::NamedNode::new_unchecked(n.as_str()))
684            }
685            crate::model::Object::BlankNode(b) => {
686                oxrdf::Term::BlankNode(oxrdf::BlankNode::new_unchecked(b.as_str()))
687            }
688            crate::model::Object::Literal(l) => {
689                let literal = if let Some(lang) = l.language() {
690                    oxrdf::Literal::new_language_tagged_literal_unchecked(l.value(), lang)
691                } else {
692                    let datatype = oxrdf::NamedNode::new_unchecked(l.datatype().as_str());
693                    oxrdf::Literal::new_typed_literal(l.value(), datatype)
694                };
695                oxrdf::Term::Literal(literal)
696            }
697            _ => {
698                return Err(crate::OxirsError::Serialize(
699                    "Variable objects and quoted triples not supported in serialization"
700                        .to_string(),
701                ))
702            }
703        };
704
705        // Convert graph name
706        let graph_name = match quad.graph_name() {
707            GraphName::NamedNode(n) => {
708                oxrdf::GraphName::NamedNode(oxrdf::NamedNode::new_unchecked(n.as_str()))
709            }
710            GraphName::BlankNode(b) => {
711                oxrdf::GraphName::BlankNode(oxrdf::BlankNode::new_unchecked(b.as_str()))
712            }
713            GraphName::DefaultGraph => oxrdf::GraphName::DefaultGraph,
714            _ => {
715                return Err(crate::OxirsError::Serialize(
716                    "Variable graph names not supported in serialization".to_string(),
717                ))
718            }
719        };
720
721        Ok(oxrdf::Quad::new(subject, predicate, object, graph_name))
722    }
723}
724
725/// Turtle serializer with prefix optimization
726struct TurtleSerializer {
727    prefixes: std::collections::HashMap<String, String>,
728    used_namespaces: std::collections::HashSet<String>,
729}
730
731impl TurtleSerializer {
732    fn new() -> Self {
733        let mut prefixes = std::collections::HashMap::new();
734
735        // Add common prefixes
736        prefixes.insert(
737            "http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
738            "rdf".to_string(),
739        );
740        prefixes.insert(
741            "http://www.w3.org/2000/01/rdf-schema#".to_string(),
742            "rdfs".to_string(),
743        );
744        prefixes.insert(
745            "http://www.w3.org/2001/XMLSchema#".to_string(),
746            "xsd".to_string(),
747        );
748        prefixes.insert("http://xmlns.com/foaf/0.1/".to_string(), "foaf".to_string());
749        prefixes.insert(
750            "http://purl.org/dc/elements/1.1/".to_string(),
751            "dc".to_string(),
752        );
753
754        TurtleSerializer {
755            prefixes,
756            used_namespaces: std::collections::HashSet::new(),
757        }
758    }
759
760    fn serialize_graph(&mut self, graph: &Graph) -> Result<String> {
761        // First pass: collect all namespaces used in the graph
762        self.collect_namespaces(graph);
763
764        let mut result = String::new();
765
766        // Write prefix declarations
767        let mut prefix_entries: Vec<_> = self.prefixes.iter().collect();
768        prefix_entries.sort_by_key(|(_, prefix)| *prefix);
769
770        for (namespace, prefix) in prefix_entries {
771            if self.used_namespaces.contains(namespace) {
772                result.push_str(&format!("@prefix {prefix}: <{namespace}> .\n"));
773            }
774        }
775
776        if !self.prefixes.is_empty() && !graph.is_empty() {
777            result.push('\n');
778        }
779
780        // Group triples by subject for better readability
781        let mut subjects_map: std::collections::HashMap<
782            crate::model::Subject,
783            Vec<&crate::model::Triple>,
784        > = std::collections::HashMap::new();
785
786        for triple in graph.iter() {
787            subjects_map
788                .entry(triple.subject().clone())
789                .or_default()
790                .push(triple);
791        }
792
793        let mut subject_entries: Vec<_> = subjects_map.iter().collect();
794        subject_entries.sort_by_key(|(subject, _)| format!("{subject}"));
795
796        for (i, (subject, triples)) in subject_entries.iter().enumerate() {
797            if i > 0 {
798                result.push('\n');
799            }
800
801            result.push_str(&self.serialize_subject(subject)?);
802
803            // Group by predicate for ; syntax
804            let mut predicates_map: std::collections::HashMap<
805                crate::model::Predicate,
806                Vec<&crate::model::Object>,
807            > = std::collections::HashMap::new();
808
809            for triple in triples.iter() {
810                predicates_map
811                    .entry(triple.predicate().clone())
812                    .or_default()
813                    .push(triple.object());
814            }
815
816            let mut predicate_entries: Vec<_> = predicates_map.iter().collect();
817            predicate_entries.sort_by_key(|(predicate, _)| format!("{predicate}"));
818
819            for (j, (predicate, objects)) in predicate_entries.iter().enumerate() {
820                if j == 0 {
821                    result.push(' ');
822                } else {
823                    result.push_str(" ;\n        ");
824                }
825
826                result.push_str(&self.serialize_predicate(predicate)?);
827                result.push(' ');
828
829                // Serialize objects with , syntax
830                for (k, object) in objects.iter().enumerate() {
831                    if k > 0 {
832                        result.push_str(", ");
833                    }
834                    result.push_str(&self.serialize_object(object)?);
835                }
836            }
837
838            result.push_str(" .\n");
839        }
840
841        Ok(result)
842    }
843
844    fn collect_namespaces(&mut self, graph: &Graph) {
845        for triple in graph.iter() {
846            self.mark_namespace_used(triple.subject());
847            self.mark_namespace_used_predicate(triple.predicate());
848            self.mark_namespace_used_object(triple.object());
849        }
850    }
851
852    fn mark_namespace_used(&mut self, subject: &crate::model::Subject) {
853        if let crate::model::Subject::NamedNode(node) = subject {
854            if let Some(namespace) = self.extract_namespace(node.as_str()) {
855                self.used_namespaces.insert(namespace);
856            }
857        }
858    }
859
860    fn mark_namespace_used_predicate(&mut self, predicate: &crate::model::Predicate) {
861        if let crate::model::Predicate::NamedNode(node) = predicate {
862            if let Some(namespace) = self.extract_namespace(node.as_str()) {
863                self.used_namespaces.insert(namespace);
864            }
865        }
866    }
867
868    fn mark_namespace_used_object(&mut self, object: &crate::model::Object) {
869        match object {
870            crate::model::Object::NamedNode(node) => {
871                if let Some(namespace) = self.extract_namespace(node.as_str()) {
872                    self.used_namespaces.insert(namespace);
873                }
874            }
875            crate::model::Object::Literal(literal) => {
876                let datatype = literal.datatype();
877                if let Some(namespace) = self.extract_namespace(datatype.as_str()) {
878                    self.used_namespaces.insert(namespace);
879                }
880            }
881            _ => {}
882        }
883    }
884
885    fn extract_namespace(&self, iri: &str) -> Option<String> {
886        // Find the namespace part of an IRI
887        if let Some(hash_pos) = iri.rfind('#') {
888            Some(format!("{}#", &iri[..hash_pos]))
889        } else {
890            iri.rfind('/')
891                .map(|slash_pos| format!("{}/", &iri[..slash_pos]))
892        }
893    }
894
895    fn serialize_subject(&self, subject: &crate::model::Subject) -> Result<String> {
896        match subject {
897            crate::model::Subject::NamedNode(node) => self.serialize_iri(node.as_str()),
898            crate::model::Subject::BlankNode(node) => Ok(node.as_str().to_string()),
899            crate::model::Subject::Variable(_var) => Err(crate::OxirsError::Serialize(
900                "Variables not supported in Turtle serialization".to_string(),
901            )),
902            crate::model::Subject::QuotedTriple(qt) => Ok(format!(
903                "<< {} {} {} >>",
904                self.serialize_subject(qt.subject())?,
905                self.serialize_predicate(qt.predicate())?,
906                self.serialize_object(qt.object())?
907            )),
908        }
909    }
910
911    fn serialize_predicate(&self, predicate: &crate::model::Predicate) -> Result<String> {
912        match predicate {
913            crate::model::Predicate::NamedNode(node) => {
914                // Check for rdf:type shorthand
915                if node.as_str() == "http://www.w3.org/1999/02/22-rdf-syntax-ns#type" {
916                    Ok("a".to_string())
917                } else {
918                    self.serialize_iri(node.as_str())
919                }
920            }
921            crate::model::Predicate::Variable(_) => Err(crate::OxirsError::Serialize(
922                "Variables not supported in Turtle serialization".to_string(),
923            )),
924        }
925    }
926
927    fn serialize_object(&self, object: &crate::model::Object) -> Result<String> {
928        match object {
929            crate::model::Object::NamedNode(node) => self.serialize_iri(node.as_str()),
930            crate::model::Object::BlankNode(node) => Ok(node.as_str().to_string()),
931            crate::model::Object::Literal(literal) => self.serialize_literal(literal),
932            crate::model::Object::Variable(_) => Err(crate::OxirsError::Serialize(
933                "Variables not supported in Turtle serialization".to_string(),
934            )),
935            crate::model::Object::QuotedTriple(qt) => Ok(format!(
936                "<< {} {} {} >>",
937                self.serialize_subject(qt.subject())?,
938                self.serialize_predicate(qt.predicate())?,
939                self.serialize_object(qt.object())?
940            )),
941        }
942    }
943
944    fn serialize_iri(&self, iri: &str) -> Result<String> {
945        // Try to use a prefix if available
946        for (namespace, prefix) in &self.prefixes {
947            if iri.starts_with(namespace) && self.used_namespaces.contains(namespace) {
948                let local_name = &iri[namespace.len()..];
949                // Ensure local name is valid
950                if self.is_valid_local_name(local_name) {
951                    return Ok(format!("{prefix}:{local_name}"));
952                }
953            }
954        }
955
956        // Fall back to full IRI
957        Ok(format!("<{iri}>"))
958    }
959
960    fn is_valid_local_name(&self, name: &str) -> bool {
961        // Simple validation - should start with letter or underscore
962        // and contain only alphanumeric, underscore, hyphen
963        if name.is_empty() {
964            return true; // Empty local name is valid
965        }
966
967        let first_char = name
968            .chars()
969            .next()
970            .expect("non-empty name should have first char");
971        if !first_char.is_alphabetic() && first_char != '_' {
972            return false;
973        }
974
975        name.chars()
976            .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
977    }
978
979    fn serialize_literal(&self, literal: &crate::model::Literal) -> Result<String> {
980        let mut result = String::new();
981
982        // Quote the literal value
983        result.push('"');
984        for c in literal.value().chars() {
985            match c {
986                '"' => result.push_str("\\\""),
987                '\\' => result.push_str("\\\\"),
988                '\n' => result.push_str("\\n"),
989                '\r' => result.push_str("\\r"),
990                '\t' => result.push_str("\\t"),
991                _ => result.push(c),
992            }
993        }
994        result.push('"');
995
996        // Add language tag or datatype
997        if let Some(lang) = literal.language() {
998            result.push_str(&format!("@{lang}"));
999        } else {
1000            let datatype = literal.datatype();
1001            // Check if it's the default string type
1002            if datatype.as_str() != "http://www.w3.org/2001/XMLSchema#string" {
1003                result.push_str("^^");
1004                result.push_str(&self.serialize_iri(datatype.as_str())?);
1005            }
1006        }
1007
1008        Ok(result)
1009    }
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014    use super::*;
1015    use crate::model::graph::Graph;
1016    use crate::model::*;
1017
1018    fn create_test_graph() -> Graph {
1019        let mut graph = Graph::new();
1020
1021        // Add a simple triple
1022        let subject = NamedNode::new("http://example.org/alice").expect("valid IRI");
1023        let predicate = NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("valid IRI");
1024        let object = Literal::new("Alice Smith");
1025        let triple1 = Triple::new(subject.clone(), predicate, object);
1026
1027        // Add a typed literal triple
1028        let age_pred = NamedNode::new("http://xmlns.com/foaf/0.1/age").expect("valid IRI");
1029        let age_obj = Literal::new_typed("30", crate::vocab::xsd::INTEGER.clone());
1030        let triple2 = Triple::new(subject.clone(), age_pred, age_obj);
1031
1032        // Add a language-tagged literal triple
1033        let desc_pred = NamedNode::new("http://example.org/description").expect("valid IRI");
1034        let desc_obj =
1035            Literal::new_lang("Une personne", "fr").expect("construction should succeed");
1036        let triple3 = Triple::new(subject, desc_pred, desc_obj);
1037
1038        // Add a blank node triple
1039        let blank_subject = BlankNode::new("person1").expect("valid blank node id");
1040        let knows_pred = NamedNode::new("http://xmlns.com/foaf/0.1/knows").expect("valid IRI");
1041        let knows_obj = NamedNode::new("http://example.org/bob").expect("valid IRI");
1042        let triple4 = Triple::new(blank_subject, knows_pred, knows_obj);
1043
1044        graph.insert(triple1);
1045        graph.insert(triple2);
1046        graph.insert(triple3);
1047        graph.insert(triple4);
1048
1049        graph
1050    }
1051
1052    #[test]
1053    fn test_ntriples_serialization() {
1054        let graph = create_test_graph();
1055        let serializer = Serializer::new(RdfFormat::NTriples);
1056
1057        let result = serializer.serialize_graph(&graph);
1058        assert!(result.is_ok());
1059
1060        let ntriples = result.expect("should have value");
1061        assert!(!ntriples.is_empty());
1062
1063        // Check that all lines end with " ."
1064        for line in ntriples.lines() {
1065            if !line.trim().is_empty() {
1066                assert!(line.ends_with(" ."), "Line should end with ' .': {line}");
1067            }
1068        }
1069
1070        // Check that the output contains our expected triples
1071        assert!(ntriples.contains("http://example.org/alice"));
1072        assert!(ntriples.contains("http://xmlns.com/foaf/0.1/name"));
1073        assert!(ntriples.contains("\"Alice Smith\""));
1074        assert!(ntriples.contains("\"30\"^^<http://www.w3.org/2001/XMLSchema#integer>"));
1075        assert!(ntriples.contains("\"Une personne\"@fr"));
1076        assert!(ntriples.contains("_:person1"));
1077    }
1078
1079    #[test]
1080    fn test_literal_escaping() {
1081        let mut graph = Graph::new();
1082        let subject = NamedNode::new("http://example.org/test").expect("valid IRI");
1083        let predicate = NamedNode::new("http://example.org/description").expect("valid IRI");
1084
1085        // Test literal with quotes and escape sequences
1086        let object = Literal::new("Text with \"quotes\" and \n newlines \t and tabs");
1087        let triple = Triple::new(subject, predicate, object);
1088        graph.insert(triple);
1089
1090        let serializer = Serializer::new(RdfFormat::NTriples);
1091        let result = serializer
1092            .serialize_graph(&graph)
1093            .expect("operation should succeed");
1094
1095        // Check that quotes and escape sequences are properly escaped
1096        assert!(result.contains("\\\"quotes\\\""));
1097        assert!(result.contains("\\n"));
1098        assert!(result.contains("\\t"));
1099    }
1100
1101    #[test]
1102    fn test_empty_graph_serialization() {
1103        let graph = Graph::new();
1104        let serializer = Serializer::new(RdfFormat::NTriples);
1105
1106        let result = serializer.serialize_graph(&graph);
1107        assert!(result.is_ok());
1108
1109        let ntriples = result.expect("should have value");
1110        assert!(ntriples.is_empty());
1111    }
1112
1113    #[test]
1114    fn test_variable_serialization_error() {
1115        let mut graph = Graph::new();
1116        let variable_subject = Variable::new("x").expect("valid variable name");
1117        let predicate = NamedNode::new("http://example.org/predicate").expect("valid IRI");
1118        let object = Literal::new("test");
1119
1120        let triple = Triple::new(variable_subject, predicate, object);
1121        graph.insert(triple);
1122
1123        let serializer = Serializer::new(RdfFormat::NTriples);
1124        let result = serializer.serialize_graph(&graph);
1125
1126        assert!(result.is_err());
1127        assert!(result
1128            .unwrap_err()
1129            .to_string()
1130            .contains("Variables not supported"));
1131    }
1132
1133    #[test]
1134    fn test_turtle_serialization() {
1135        let graph = create_test_graph();
1136        let serializer = Serializer::new(RdfFormat::Turtle);
1137
1138        let result = serializer.serialize_graph(&graph);
1139        assert!(result.is_ok());
1140
1141        let turtle = result.expect("should have value");
1142        assert!(!turtle.is_empty());
1143
1144        // Should contain prefix declarations
1145        assert!(turtle.contains("@prefix"));
1146
1147        // Should contain the 'a' shorthand for rdf:type
1148        // Should use abbreviated syntax with ;
1149
1150        // Should have proper Turtle syntax
1151        assert!(turtle.ends_with(" .\n") || turtle.ends_with("."));
1152    }
1153
1154    #[test]
1155    fn test_turtle_serialization_with_prefixes() {
1156        let mut graph = Graph::new();
1157
1158        // Create triples using FOAF vocabulary
1159        let alice = NamedNode::new("http://example.org/alice").expect("valid IRI");
1160        let name_pred = NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("valid IRI");
1161        let person_type = NamedNode::new("http://xmlns.com/foaf/0.1/Person").expect("valid IRI");
1162        let rdf_type =
1163            NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type").expect("valid IRI");
1164
1165        let name_literal = Literal::new("Alice");
1166
1167        graph.insert(Triple::new(alice.clone(), name_pred, name_literal));
1168        graph.insert(Triple::new(alice, rdf_type, person_type));
1169
1170        let serializer = Serializer::new(RdfFormat::Turtle);
1171        let turtle = serializer
1172            .serialize_graph(&graph)
1173            .expect("operation should succeed");
1174
1175        // Should include FOAF prefix
1176        assert!(turtle.contains("@prefix foaf: <http://xmlns.com/foaf/0.1/>"));
1177
1178        // Should use 'a' shorthand for rdf:type
1179        assert!(turtle.contains(" a "));
1180
1181        // Should use prefixed names
1182        assert!(turtle.contains("foaf:"));
1183    }
1184
1185    #[test]
1186    fn test_turtle_serialization_abbreviated_syntax() {
1187        let mut graph = Graph::new();
1188
1189        let alice = NamedNode::new("http://example.org/alice").expect("valid IRI");
1190        let name_pred = NamedNode::new("http://xmlns.com/foaf/0.1/name").expect("valid IRI");
1191        let age_pred = NamedNode::new("http://xmlns.com/foaf/0.1/age").expect("valid IRI");
1192
1193        let name_literal = Literal::new("Alice");
1194        let age_literal = Literal::new_typed("30", crate::vocab::xsd::INTEGER.clone());
1195
1196        graph.insert(Triple::new(alice.clone(), name_pred, name_literal));
1197        graph.insert(Triple::new(alice, age_pred, age_literal));
1198
1199        let serializer = Serializer::new(RdfFormat::Turtle);
1200        let turtle = serializer
1201            .serialize_graph(&graph)
1202            .expect("operation should succeed");
1203
1204        // Should use ; syntax for same subject
1205        assert!(turtle.contains(";"));
1206
1207        // Should have proper indentation and formatting
1208        assert!(turtle.lines().count() >= 3); // prefixes + subject line + continuation
1209    }
1210
1211    #[test]
1212    fn test_turtle_serialization_literals() {
1213        let mut graph = Graph::new();
1214
1215        let subject = NamedNode::new("http://example.org/test").expect("valid IRI");
1216        let desc_pred = NamedNode::new("http://example.org/description").expect("valid IRI");
1217        let age_pred = NamedNode::new("http://example.org/age").expect("valid IRI");
1218
1219        // Language-tagged literal
1220        let desc_literal =
1221            Literal::new_lang("Une description", "fr").expect("construction should succeed");
1222        // Typed literal
1223        let age_literal = Literal::new_typed("25", crate::vocab::xsd::INTEGER.clone());
1224
1225        graph.insert(Triple::new(subject.clone(), desc_pred, desc_literal));
1226        graph.insert(Triple::new(subject, age_pred, age_literal));
1227
1228        let serializer = Serializer::new(RdfFormat::Turtle);
1229        let turtle = serializer
1230            .serialize_graph(&graph)
1231            .expect("operation should succeed");
1232
1233        // Should contain language tag
1234        assert!(turtle.contains("@fr"));
1235
1236        // Should contain datatype with xsd prefix
1237        assert!(turtle.contains("^^xsd:integer"));
1238    }
1239}