Skip to main content

oxirs_stream/patch/
serializer.rs

1//! RDF Patch serializer
2
3use crate::{PatchOperation, RdfPatch};
4use anyhow::Result;
5use std::collections::HashMap;
6
7pub struct PatchSerializer {
8    pretty_print: bool,
9    include_metadata: bool,
10    prefixes: HashMap<String, String>,
11}
12
13impl PatchSerializer {
14    pub fn new() -> Self {
15        let mut prefixes = HashMap::new();
16        prefixes.insert(
17            "rdf".to_string(),
18            "http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
19        );
20        prefixes.insert(
21            "rdfs".to_string(),
22            "http://www.w3.org/2000/01/rdf-schema#".to_string(),
23        );
24        prefixes.insert(
25            "xsd".to_string(),
26            "http://www.w3.org/2001/XMLSchema#".to_string(),
27        );
28
29        Self {
30            pretty_print: true,
31            include_metadata: true,
32            prefixes,
33        }
34    }
35
36    pub fn with_pretty_print(mut self, pretty: bool) -> Self {
37        self.pretty_print = pretty;
38        self
39    }
40
41    pub fn with_metadata(mut self, include: bool) -> Self {
42        self.include_metadata = include;
43        self
44    }
45
46    pub fn add_prefix(&mut self, prefix: String, namespace: String) {
47        self.prefixes.insert(prefix, namespace);
48    }
49
50    /// Serialize RDF Patch to string
51    pub fn serialize(&self, patch: &RdfPatch) -> Result<String> {
52        let mut output = String::new();
53
54        // Write header
55        if self.include_metadata {
56            output.push_str("# RDF Patch\n");
57            output.push_str(&format!(
58                "# Generated: {}\n",
59                patch.timestamp.format("%Y-%m-%d %H:%M:%S UTC")
60            ));
61            output.push_str(&format!("# Patch ID: {}\n", patch.id));
62            output.push_str(&format!("# Operations: {}\n", patch.operations.len()));
63            output.push('\n');
64        }
65
66        // Write prefixes
67        if self.pretty_print {
68            for (prefix, namespace) in &self.prefixes {
69                output.push_str(&format!("@prefix {prefix}: <{namespace}> .\n"));
70            }
71            if !self.prefixes.is_empty() {
72                output.push('\n');
73            }
74        }
75
76        // Write operations
77        for (i, operation) in patch.operations.iter().enumerate() {
78            let op_str = self.serialize_operation(operation)?;
79            output.push_str(&op_str);
80            output.push('\n');
81
82            // Add spacing for readability in pretty print mode
83            if self.pretty_print && i > 0 && i % 10 == 0 {
84                output.push('\n');
85            }
86        }
87
88        Ok(output)
89    }
90
91    fn serialize_operation(&self, operation: &PatchOperation) -> Result<String> {
92        match operation {
93            PatchOperation::Add {
94                subject,
95                predicate,
96                object,
97            } => {
98                let s = self.compact_term(subject);
99                let p = self.compact_term(predicate);
100                let o = self.compact_term(object);
101                Ok(format!("A {s} {p} {o} ."))
102            }
103            PatchOperation::Delete {
104                subject,
105                predicate,
106                object,
107            } => {
108                let s = self.compact_term(subject);
109                let p = self.compact_term(predicate);
110                let o = self.compact_term(object);
111                Ok(format!("D {s} {p} {o} ."))
112            }
113            PatchOperation::AddGraph { graph } => {
114                let g = self.compact_term(graph);
115                Ok(format!("GA {g} ."))
116            }
117            PatchOperation::DeleteGraph { graph } => {
118                let g = self.compact_term(graph);
119                Ok(format!("GD {g} ."))
120            }
121            PatchOperation::AddPrefix { prefix, namespace } => {
122                Ok(format!("PA {prefix}: <{namespace}> ."))
123            }
124            PatchOperation::DeletePrefix { prefix } => Ok(format!("PD {prefix}: .")),
125            PatchOperation::TransactionBegin { transaction_id } => {
126                if let Some(id) = transaction_id {
127                    Ok(format!("TX {id} ."))
128                } else {
129                    Ok("TX .".to_string())
130                }
131            }
132            PatchOperation::TransactionCommit => Ok("TC .".to_string()),
133            PatchOperation::TransactionAbort => Ok("TA .".to_string()),
134            PatchOperation::Header { key, value } => Ok(format!("H {key} {value} .")),
135        }
136    }
137
138    fn compact_term(&self, term: &str) -> String {
139        // Try to find a prefix that matches
140        for (prefix, namespace) in &self.prefixes {
141            if term.starts_with(namespace) {
142                let local = &term[namespace.len()..];
143                return format!("{prefix}:{local}");
144            }
145        }
146
147        // If no prefix matches, use full URI notation
148        if term.starts_with('"') || term.starts_with('_') {
149            // Literal or blank node - return as-is
150            term.to_string()
151        } else {
152            // Wrap in angle brackets for full URI
153            format!("<{term}>")
154        }
155    }
156}
157
158impl Default for PatchSerializer {
159    fn default() -> Self {
160        Self::new()
161    }
162}