oxirs_stream/patch/
serializer.rs1use 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 pub fn serialize(&self, patch: &RdfPatch) -> Result<String> {
52 let mut output = String::new();
53
54 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 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 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 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 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 term.starts_with('"') || term.starts_with('_') {
149 term.to_string()
151 } else {
152 format!("<{term}>")
154 }
155 }
156}
157
158impl Default for PatchSerializer {
159 fn default() -> Self {
160 Self::new()
161 }
162}