Skip to main content

oxirs_core/format/
serializer.rs

1//! Unified RDF Serializer Interface
2//!
3//! Provides a consistent API for serializing to all supported RDF formats.
4//! Extracted and adapted from OxiGraph with OxiRS enhancements.
5
6use super::error::FormatError;
7pub use super::error::SerializeResult;
8use super::format::RdfFormat;
9use crate::model::{Quad, QuadRef, Triple, TripleRef};
10use std::collections::HashMap;
11use std::io::Write;
12
13/// Result type for quad serialization operations
14pub type QuadSerializeResult = SerializeResult<()>;
15
16/// Writer-based quad serializer
17pub struct WriterQuadSerializer<W: Write> {
18    inner: Box<dyn QuadSerializer<W>>,
19}
20
21impl<W: Write> WriterQuadSerializer<W> {
22    /// Create a new writer serializer
23    pub fn new(serializer: Box<dyn QuadSerializer<W>>) -> Self {
24        Self { inner: serializer }
25    }
26
27    /// Serialize a quad
28    pub fn serialize_quad<'a>(&mut self, quad: impl Into<QuadRef<'a>>) -> QuadSerializeResult {
29        self.inner.serialize_quad(quad.into())
30    }
31
32    /// Serialize a triple (placed in default graph)
33    pub fn serialize_triple<'a>(
34        &mut self,
35        triple: impl Into<TripleRef<'a>>,
36    ) -> QuadSerializeResult {
37        let quad = triple.into().in_graph(None);
38        self.serialize_quad(quad)
39    }
40
41    /// Serialize multiple quads
42    pub fn serialize_quads<I>(&mut self, quads: I) -> QuadSerializeResult
43    where
44        I: IntoIterator,
45        I::Item: Into<QuadRef<'static>>,
46    {
47        for quad in quads {
48            self.inner.serialize_quad(quad.into())?;
49        }
50        Ok(())
51    }
52
53    /// Finish serialization and return the writer
54    pub fn finish(self) -> SerializeResult<W> {
55        self.inner.finish()
56    }
57}
58
59/// Trait for serializing quads to a writer
60pub trait QuadSerializer<W: Write> {
61    /// Serialize a quad
62    fn serialize_quad(&mut self, quad: QuadRef<'_>) -> QuadSerializeResult;
63
64    /// Finish serialization and return the writer
65    fn finish(self: Box<Self>) -> SerializeResult<W>;
66}
67
68/// Extension trait for bulk serialization operations
69pub trait QuadSerializerExt<W: Write>: QuadSerializer<W> {
70    /// Serialize multiple quads
71    fn serialize_quads<I>(&mut self, quads: I) -> QuadSerializeResult
72    where
73        I: IntoIterator,
74        I::Item: Into<QuadRef<'static>>,
75    {
76        for quad in quads {
77            self.serialize_quad(quad.into())?;
78        }
79        Ok(())
80    }
81}
82
83/// Blanket implementation for all QuadSerializer types
84impl<W: Write, T: QuadSerializer<W>> QuadSerializerExt<W> for T {}
85
86/// Unified RDF serializer supporting all formats
87pub struct RdfSerializer {
88    format: RdfFormat,
89    base_iri: Option<String>,
90    prefixes: HashMap<String, String>,
91    pretty: bool,
92}
93
94impl RdfSerializer {
95    /// Create a new serializer for the specified format
96    pub fn new(format: RdfFormat) -> Self {
97        Self {
98            format,
99            base_iri: None,
100            prefixes: HashMap::new(),
101            pretty: false,
102        }
103    }
104
105    /// Set the base IRI for relative IRI generation
106    pub fn with_base_iri(mut self, base_iri: impl Into<String>) -> Self {
107        self.base_iri = Some(base_iri.into());
108        self
109    }
110
111    /// Add a namespace prefix
112    pub fn with_prefix(mut self, prefix: impl Into<String>, iri: impl Into<String>) -> Self {
113        self.prefixes.insert(prefix.into(), iri.into());
114        self
115    }
116
117    /// Enable pretty formatting (indentation, line breaks)
118    pub fn pretty(mut self) -> Self {
119        self.pretty = true;
120        self
121    }
122
123    /// Create a writer-based serializer
124    pub fn for_writer<W: Write + 'static>(self, writer: W) -> WriterQuadSerializer<W> {
125        match self.format {
126            RdfFormat::Turtle => self.create_turtle_serializer(writer),
127            RdfFormat::NTriples => self.create_ntriples_serializer(writer),
128            RdfFormat::NQuads => self.create_nquads_serializer(writer),
129            RdfFormat::TriG => self.create_trig_serializer(writer),
130            RdfFormat::RdfXml => self.create_rdfxml_serializer(writer),
131            RdfFormat::JsonLd { .. } => self.create_jsonld_serializer(writer),
132            RdfFormat::N3 => self.create_n3_serializer(writer),
133        }
134    }
135
136    /// Get the format being serialized
137    pub fn format(&self) -> RdfFormat {
138        self.format.clone()
139    }
140
141    /// Get the base IRI
142    pub fn base_iri(&self) -> Option<&str> {
143        self.base_iri.as_deref()
144    }
145
146    /// Get the prefixes
147    pub fn prefixes(&self) -> &HashMap<String, String> {
148        &self.prefixes
149    }
150
151    /// Check if pretty formatting is enabled
152    pub fn is_pretty(&self) -> bool {
153        self.pretty
154    }
155
156    // Format-specific serializer implementations
157
158    fn create_turtle_serializer<W: Write + 'static>(self, writer: W) -> WriterQuadSerializer<W> {
159        // Use existing Turtle serializer implementation
160        let mut turtle_serializer = super::turtle::TurtleSerializer::new();
161
162        // Apply configuration
163        if let Some(base) = self.base_iri {
164            turtle_serializer = turtle_serializer.with_base_iri(&base);
165        }
166        for (prefix, iri) in self.prefixes {
167            turtle_serializer = turtle_serializer.with_prefix(&prefix, &iri);
168        }
169        if self.pretty {
170            turtle_serializer = turtle_serializer.pretty();
171        }
172
173        WriterQuadSerializer::new(Box::new(turtle_serializer.for_writer(writer)))
174    }
175
176    fn create_ntriples_serializer<W: Write + 'static>(self, writer: W) -> WriterQuadSerializer<W> {
177        // Use existing N-Triples serializer implementation
178        let ntriples_serializer = super::ntriples::NTriplesSerializer::new().for_writer(writer);
179        WriterQuadSerializer::new(Box::new(ntriples_serializer))
180    }
181
182    fn create_nquads_serializer<W: Write + 'static>(self, writer: W) -> WriterQuadSerializer<W> {
183        // Use N-Quads serializer implementation
184        let nquads_serializer = super::nquads::NQuadsSerializer::new().for_writer(writer);
185        WriterQuadSerializer::new(Box::new(nquads_serializer))
186    }
187
188    fn create_trig_serializer<W: Write + 'static>(self, writer: W) -> WriterQuadSerializer<W> {
189        // Use TriG serializer implementation
190        let mut trig_serializer = super::trig::TriGSerializer::new();
191
192        // Apply configuration
193        if let Some(base) = self.base_iri {
194            trig_serializer = trig_serializer.with_base_iri(&base);
195        }
196        for (prefix, iri) in self.prefixes {
197            trig_serializer = trig_serializer.with_prefix(&prefix, &iri);
198        }
199        if self.pretty {
200            trig_serializer = trig_serializer.pretty();
201        }
202
203        WriterQuadSerializer::new(Box::new(trig_serializer.for_writer(writer)))
204    }
205
206    fn create_rdfxml_serializer<W: Write + 'static>(self, writer: W) -> WriterQuadSerializer<W> {
207        // Use existing RDF/XML serializer implementation
208        let mut rdfxml_serializer = super::rdfxml::RdfXmlSerializer::new();
209
210        // Apply configuration
211        if self.pretty {
212            rdfxml_serializer = rdfxml_serializer.pretty();
213        }
214
215        WriterQuadSerializer::new(Box::new(rdfxml_serializer.for_writer(writer)))
216    }
217
218    fn create_jsonld_serializer<W: Write + 'static>(self, writer: W) -> WriterQuadSerializer<W> {
219        // Use existing JSON-LD serializer implementation
220        let mut jsonld_serializer = super::jsonld::JsonLdSerializer::new();
221
222        // Apply configuration
223        if self.pretty {
224            jsonld_serializer = jsonld_serializer.pretty();
225        }
226
227        WriterQuadSerializer::new(Box::new(jsonld_serializer.for_writer(writer)))
228    }
229
230    fn create_n3_serializer<W: Write + 'static>(self, writer: W) -> WriterQuadSerializer<W> {
231        // Use N3 serializer implementation
232        let mut n3_serializer = super::n3::N3Serializer::new();
233
234        // Apply configuration
235        if let Some(base) = self.base_iri {
236            n3_serializer = n3_serializer.with_base_iri(&base);
237        }
238        for (prefix, iri) in self.prefixes {
239            n3_serializer = n3_serializer.with_prefix(&prefix, &iri);
240        }
241        if self.pretty {
242            n3_serializer = n3_serializer.pretty();
243        }
244
245        WriterQuadSerializer::new(Box::new(n3_serializer.for_writer(writer)))
246    }
247}
248
249impl Default for RdfSerializer {
250    fn default() -> Self {
251        Self::new(RdfFormat::default())
252    }
253}
254
255/// Serialization configuration for fine-grained control
256#[derive(Debug, Clone)]
257pub struct SerializeConfig {
258    /// Use compact formatting
259    pub compact: bool,
260    /// Indentation string for pretty formatting
261    pub indent: String,
262    /// Line ending style
263    pub line_ending: LineEnding,
264    /// Maximum line length for wrapping
265    pub max_line_length: Option<usize>,
266    /// Sort output by subject/predicate
267    pub sort_output: bool,
268    /// Include comments in output
269    pub include_comments: bool,
270    /// Validate output during serialization
271    pub validate_output: bool,
272}
273
274/// Line ending styles
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub enum LineEnding {
277    /// Unix-style line endings (\n)
278    Unix,
279    /// Windows-style line endings (\r\n)
280    Windows,
281    /// Mac-style line endings (\r)
282    Mac,
283    /// Platform-default line endings
284    Platform,
285}
286
287impl Default for SerializeConfig {
288    fn default() -> Self {
289        Self {
290            compact: false,
291            indent: "  ".to_string(),
292            line_ending: LineEnding::Platform,
293            max_line_length: None,
294            sort_output: false,
295            include_comments: false,
296            validate_output: true,
297        }
298    }
299}
300
301impl LineEnding {
302    /// Get the line ending string
303    pub fn as_str(&self) -> &'static str {
304        match self {
305            Self::Unix => "\n",
306            Self::Windows => "\r\n",
307            Self::Mac => "\r",
308            Self::Platform => {
309                #[cfg(windows)]
310                return "\r\n";
311                #[cfg(not(windows))]
312                return "\n";
313            }
314        }
315    }
316}
317
318/// A [`Write`] adapter that buffers all output in memory so it can be
319/// re-sorted (line-wise) and/or re-terminated with a custom line ending
320/// before being flushed to the wrapped writer on `finalize`.
321pub struct PostProcessingWriter<W: Write> {
322    inner: W,
323    buffer: Vec<u8>,
324    line_ending: LineEnding,
325    sort_output: bool,
326}
327
328impl<W: Write> PostProcessingWriter<W> {
329    fn new(inner: W, line_ending: LineEnding, sort_output: bool) -> Self {
330        Self {
331            inner,
332            buffer: Vec::new(),
333            line_ending,
334            sort_output,
335        }
336    }
337
338    /// Flush the buffered output (optionally sorted) to the inner writer,
339    /// using this instance's configured line ending, and return the inner
340    /// writer.
341    fn finalize(mut self) -> std::io::Result<W> {
342        let text = String::from_utf8_lossy(&self.buffer);
343        // Split into logical lines, dropping a single trailing empty
344        // segment produced by a final line terminator.
345        let mut lines: Vec<&str> = text.lines().collect();
346        if self.sort_output {
347            lines.sort_unstable();
348        }
349        let ending = self.line_ending.as_str();
350        for line in &lines {
351            self.inner.write_all(line.as_bytes())?;
352            self.inner.write_all(ending.as_bytes())?;
353        }
354        self.inner.flush()?;
355        Ok(self.inner)
356    }
357}
358
359impl<W: Write> Write for PostProcessingWriter<W> {
360    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
361        self.buffer.extend_from_slice(buf);
362        Ok(buf.len())
363    }
364
365    fn flush(&mut self) -> std::io::Result<()> {
366        // Nothing is written to the inner writer until finalize(); this is
367        // intentional since output must be fully buffered before it can be
368        // sorted or re-terminated.
369        Ok(())
370    }
371}
372
373/// A quad serializer returned by [`ConfigurableSerializer::for_writer`].
374///
375/// Wraps either a direct [`WriterQuadSerializer`] (when no post-processing
376/// is required) or one operating over a [`PostProcessingWriter`] (when
377/// `sort_output` and/or a custom `line_ending` was requested).
378pub enum ConfiguredQuadSerializer<W: Write> {
379    /// No post-processing: writes flow straight through to `W`.
380    Direct(WriterQuadSerializer<W>),
381    /// Buffered: writes are collected, then sorted/re-terminated and
382    /// flushed to `W` on `finish()`.
383    Buffered(WriterQuadSerializer<PostProcessingWriter<W>>),
384}
385
386impl<W: Write + 'static> ConfiguredQuadSerializer<W> {
387    /// Serialize a quad.
388    pub fn serialize_quad<'a>(&mut self, quad: impl Into<QuadRef<'a>>) -> QuadSerializeResult {
389        match self {
390            Self::Direct(s) => s.serialize_quad(quad),
391            Self::Buffered(s) => s.serialize_quad(quad),
392        }
393    }
394
395    /// Serialize a triple (placed in default graph).
396    pub fn serialize_triple<'a>(
397        &mut self,
398        triple: impl Into<TripleRef<'a>>,
399    ) -> QuadSerializeResult {
400        match self {
401            Self::Direct(s) => s.serialize_triple(triple),
402            Self::Buffered(s) => s.serialize_triple(triple),
403        }
404    }
405
406    /// Finish serialization, applying any configured post-processing, and
407    /// return the original writer.
408    pub fn finish(self) -> SerializeResult<W> {
409        match self {
410            Self::Direct(s) => s.finish(),
411            Self::Buffered(s) => {
412                let post_writer = s.finish()?;
413                post_writer.finalize()
414            }
415        }
416    }
417}
418
419/// Advanced serializer with configuration support
420pub struct ConfigurableSerializer {
421    serializer: RdfSerializer,
422    config: SerializeConfig,
423}
424
425impl ConfigurableSerializer {
426    /// Create a new configurable serializer
427    pub fn new(format: RdfFormat, config: SerializeConfig) -> Self {
428        Self {
429            serializer: RdfSerializer::new(format),
430            config,
431        }
432    }
433
434    /// Set base IRI
435    pub fn with_base_iri(mut self, base_iri: impl Into<String>) -> Self {
436        self.serializer = self.serializer.with_base_iri(base_iri);
437        self
438    }
439
440    /// Add prefix
441    pub fn with_prefix(mut self, prefix: impl Into<String>, iri: impl Into<String>) -> Self {
442        self.serializer = self.serializer.with_prefix(prefix, iri);
443        self
444    }
445
446    /// Create a writer with configuration.
447    ///
448    /// When `sort_output` is enabled or a non-platform `line_ending` is
449    /// requested, output is buffered line-by-line so it can be reordered
450    /// and re-terminated before being flushed to `writer` on `finish()`.
451    /// Line-oriented formats (N-Triples/N-Quads/Turtle/TriG/N3) benefit
452    /// from this; structural formats (RDF/XML, JSON-LD) do not have a
453    /// meaningful "line" concept, so `sort_output` is a no-op for them
454    /// (their statements are not one-per-line) while `line_ending` still
455    /// normalizes the terminator of whatever lines they do emit.
456    ///
457    /// `indent`, `max_line_length`, and `include_comments` remain
458    /// reserved: applying them safely requires format-aware rendering
459    /// (e.g. re-wrapping Turtle predicate lists) rather than a
460    /// post-processing pass, and are tracked as follow-up work.
461    pub fn for_writer<W: Write + 'static>(self, writer: W) -> ConfiguredQuadSerializer<W> {
462        // Apply configuration settings and create serializer
463        let mut serializer = self.serializer;
464
465        // Apply pretty formatting (negation of compact)
466        if !self.config.compact {
467            serializer = serializer.pretty();
468        }
469
470        let needs_post_processing =
471            self.config.sort_output || self.config.line_ending != LineEnding::Platform;
472
473        if needs_post_processing {
474            let post_writer =
475                PostProcessingWriter::new(writer, self.config.line_ending, self.config.sort_output);
476            ConfiguredQuadSerializer::Buffered(serializer.for_writer(post_writer))
477        } else {
478            ConfiguredQuadSerializer::Direct(serializer.for_writer(writer))
479        }
480    }
481
482    /// Get the configuration
483    pub fn config(&self) -> &SerializeConfig {
484        &self.config
485    }
486
487    /// Get the serializer
488    pub fn serializer(&self) -> &RdfSerializer {
489        &self.serializer
490    }
491}
492
493/// Simple serialization functions for common use cases
494pub mod simple {
495    use super::*;
496
497    /// Serialize triples to a string in the specified format
498    pub fn serialize_triples_to_string(
499        triples: &[Triple],
500        format: RdfFormat,
501    ) -> Result<String, FormatError> {
502        let buffer = Vec::new();
503        let mut serializer = RdfSerializer::new(format).for_writer(buffer);
504        for triple in triples {
505            serializer.serialize_triple(triple.as_ref())?;
506        }
507        let buffer = serializer.finish()?;
508        String::from_utf8(buffer).map_err(|e| FormatError::invalid_data(e.to_string()))
509    }
510
511    /// Serialize quads to a string in the specified format
512    pub fn serialize_quads_to_string(
513        quads: &[Quad],
514        format: RdfFormat,
515    ) -> Result<String, FormatError> {
516        let buffer = Vec::new();
517        let mut serializer = RdfSerializer::new(format).for_writer(buffer);
518        for quad in quads {
519            serializer.serialize_quad(quad.as_ref())?;
520        }
521        let buffer = serializer.finish()?;
522        String::from_utf8(buffer).map_err(|e| FormatError::invalid_data(e.to_string()))
523    }
524
525    /// Serialize triples to Turtle string
526    pub fn serialize_turtle(triples: &[Triple]) -> Result<String, FormatError> {
527        serialize_triples_to_string(triples, RdfFormat::Turtle)
528    }
529
530    /// Serialize triples to N-Triples string
531    pub fn serialize_ntriples(triples: &[Triple]) -> Result<String, FormatError> {
532        serialize_triples_to_string(triples, RdfFormat::NTriples)
533    }
534
535    /// Serialize quads to N-Quads string
536    pub fn serialize_nquads(quads: &[Quad]) -> Result<String, FormatError> {
537        serialize_quads_to_string(quads, RdfFormat::NQuads)
538    }
539
540    /// Serialize quads to TriG string
541    pub fn serialize_trig(quads: &[Quad]) -> Result<String, FormatError> {
542        serialize_quads_to_string(quads, RdfFormat::TriG)
543    }
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549
550    #[test]
551    fn test_serializer_creation() {
552        let serializer = RdfSerializer::new(RdfFormat::Turtle);
553        assert_eq!(serializer.format(), RdfFormat::Turtle);
554        assert!(serializer.base_iri().is_none());
555        assert!(serializer.prefixes().is_empty());
556        assert!(!serializer.is_pretty());
557    }
558
559    #[test]
560    fn test_serializer_configuration() {
561        let serializer = RdfSerializer::new(RdfFormat::Turtle)
562            .with_base_iri("http://example.org/")
563            .with_prefix("ex", "http://example.org/ns#")
564            .pretty();
565
566        assert_eq!(serializer.base_iri(), Some("http://example.org/"));
567        assert_eq!(
568            serializer.prefixes().get("ex"),
569            Some(&"http://example.org/ns#".to_string())
570        );
571        assert!(serializer.is_pretty());
572    }
573
574    #[test]
575    fn test_configurable_serializer() {
576        let config = SerializeConfig {
577            compact: true,
578            sort_output: true,
579            ..Default::default()
580        };
581
582        let serializer = ConfigurableSerializer::new(RdfFormat::NQuads, config);
583        assert!(serializer.config().compact);
584        assert!(serializer.config().sort_output);
585    }
586
587    #[test]
588    fn test_serialize_config_default() {
589        let config = SerializeConfig::default();
590        assert!(!config.compact);
591        assert_eq!(config.indent, "  ");
592        assert_eq!(config.line_ending, LineEnding::Platform);
593        assert_eq!(config.max_line_length, None);
594        assert!(!config.sort_output);
595        assert!(!config.include_comments);
596        assert!(config.validate_output);
597    }
598
599    #[test]
600    fn test_line_ending() {
601        assert_eq!(LineEnding::Unix.as_str(), "\n");
602        assert_eq!(LineEnding::Windows.as_str(), "\r\n");
603        assert_eq!(LineEnding::Mac.as_str(), "\r");
604        // Platform depends on the compilation target
605    }
606
607    #[test]
608    fn test_configurable_serializer_sort_output_applied() {
609        use crate::model::{NamedNode, Triple};
610
611        let s = |n: &str| NamedNode::new(format!("http://example.org/{n}")).unwrap();
612        let triples = vec![
613            Triple::new(s("c"), s("p"), s("z")),
614            Triple::new(s("a"), s("p"), s("z")),
615            Triple::new(s("b"), s("p"), s("z")),
616        ];
617
618        let config = SerializeConfig {
619            sort_output: true,
620            line_ending: LineEnding::Unix,
621            ..Default::default()
622        };
623        let mut serializer =
624            ConfigurableSerializer::new(RdfFormat::NTriples, config).for_writer(Vec::new());
625        for triple in &triples {
626            serializer.serialize_triple(triple.as_ref()).unwrap();
627        }
628        let buffer = serializer.finish().unwrap();
629        let output = String::from_utf8(buffer).unwrap();
630        let lines: Vec<&str> = output.lines().collect();
631
632        // Output must be sorted lexicographically, not insertion order.
633        let mut expected = lines.clone();
634        expected.sort_unstable();
635        assert_eq!(lines, expected);
636        assert!(lines[0].contains("/a>"));
637        assert!(lines[2].contains("/c>"));
638        // Custom line ending was honored (Unix => bare \n, no \r).
639        assert!(!output.contains('\r'));
640    }
641
642    #[test]
643    fn test_configurable_serializer_windows_line_ending() {
644        use crate::model::{NamedNode, Triple};
645
646        let s = |n: &str| NamedNode::new(format!("http://example.org/{n}")).unwrap();
647        let triple = Triple::new(s("s"), s("p"), s("o"));
648
649        let config = SerializeConfig {
650            line_ending: LineEnding::Windows,
651            ..Default::default()
652        };
653        let mut serializer =
654            ConfigurableSerializer::new(RdfFormat::NTriples, config).for_writer(Vec::new());
655        serializer.serialize_triple(triple.as_ref()).unwrap();
656        let buffer = serializer.finish().unwrap();
657        let output = String::from_utf8(buffer).unwrap();
658        assert!(output.ends_with("\r\n"));
659    }
660}