oxirs_stream/patch/
normalizer.rs1use crate::{PatchOperation, RdfPatch};
4use anyhow::Result;
5use std::collections::BTreeSet;
6use tracing::info;
7
8pub struct PatchNormalizer {
9 canonical_ordering: bool,
10 deduplicate_operations: bool,
11 normalize_uris: bool,
12 sort_by_subject: bool,
13}
14
15impl PatchNormalizer {
16 pub fn new() -> Self {
17 Self {
18 canonical_ordering: true,
19 deduplicate_operations: true,
20 normalize_uris: true,
21 sort_by_subject: true,
22 }
23 }
24
25 pub fn with_canonical_ordering(mut self, enabled: bool) -> Self {
26 self.canonical_ordering = enabled;
27 self
28 }
29
30 pub fn with_deduplication(mut self, enabled: bool) -> Self {
31 self.deduplicate_operations = enabled;
32 self
33 }
34
35 pub fn with_uri_normalization(mut self, enabled: bool) -> Self {
36 self.normalize_uris = enabled;
37 self
38 }
39
40 pub fn with_subject_sort(mut self, enabled: bool) -> Self {
46 self.sort_by_subject = enabled;
47 self
48 }
49
50 pub fn normalize(&self, patch: &RdfPatch) -> Result<RdfPatch> {
52 let mut normalized = patch.clone();
53 normalized.id = format!("{}-normalized", patch.id);
54
55 if self.normalize_uris {
57 normalized = self.normalize_uris_in_patch(normalized)?;
58 }
59
60 if self.deduplicate_operations {
62 normalized = self.deduplicate_operations_in_patch(normalized)?;
63 }
64
65 if self.canonical_ordering {
67 normalized = self.apply_canonical_ordering(normalized)?;
68 }
69
70 if self.sort_by_subject {
72 normalized = self.sort_operations_by_subject(normalized)?;
73 }
74
75 info!(
76 "Normalized patch: {} -> {} operations",
77 patch.operations.len(),
78 normalized.operations.len()
79 );
80 Ok(normalized)
81 }
82
83 fn normalize_uris_in_patch(&self, mut patch: RdfPatch) -> Result<RdfPatch> {
84 for operation in &mut patch.operations {
85 match operation {
86 PatchOperation::Add {
87 subject,
88 predicate,
89 object,
90 } => {
91 *subject = self.normalize_uri(subject);
92 *predicate = self.normalize_uri(predicate);
93 *object = self.normalize_uri(object);
94 }
95 PatchOperation::Delete {
96 subject,
97 predicate,
98 object,
99 } => {
100 *subject = self.normalize_uri(subject);
101 *predicate = self.normalize_uri(predicate);
102 *object = self.normalize_uri(object);
103 }
104 PatchOperation::AddGraph { graph } => {
105 *graph = self.normalize_uri(graph);
106 }
107 PatchOperation::DeleteGraph { graph } => {
108 *graph = self.normalize_uri(graph);
109 }
110 _ => {} }
112 }
113 Ok(patch)
114 }
115
116 fn normalize_uri(&self, uri: &str) -> String {
117 let mut normalized = uri.trim_end_matches('/').to_string();
119
120 if normalized.starts_with("http://") || normalized.starts_with("https://") {
122 if let Some(pos) = normalized.find("://") {
123 let (scheme, rest) = normalized.split_at(pos + 3);
124 if let Some(domain_end) = rest.find('/') {
125 let (domain, path) = rest.split_at(domain_end);
126 normalized =
127 format!("{}{}{}", scheme.to_lowercase(), domain.to_lowercase(), path);
128 } else {
129 normalized = format!("{}{}", scheme.to_lowercase(), rest.to_lowercase());
130 }
131 }
132 }
133
134 normalized
135 }
136
137 fn deduplicate_operations_in_patch(&self, mut patch: RdfPatch) -> Result<RdfPatch> {
138 let mut seen = BTreeSet::new();
139 patch.operations.retain(|op| {
140 let key = format!("{op:?}");
141 if seen.contains(&key) {
142 false
143 } else {
144 seen.insert(key);
145 true
146 }
147 });
148 Ok(patch)
149 }
150
151 fn apply_canonical_ordering(&self, mut patch: RdfPatch) -> Result<RdfPatch> {
152 let mut headers = Vec::new();
154 let mut prefixes = Vec::new();
155 let mut tx_begin = Vec::new();
156 let mut adds = Vec::new();
157 let mut deletes = Vec::new();
158 let mut graphs = Vec::new();
159 let mut tx_end = Vec::new();
160
161 for operation in &patch.operations {
162 match operation {
163 PatchOperation::Header { .. } => headers.push(operation.clone()),
164 PatchOperation::AddPrefix { .. } | PatchOperation::DeletePrefix { .. } => {
165 prefixes.push(operation.clone())
166 }
167 PatchOperation::TransactionBegin { .. } => tx_begin.push(operation.clone()),
168 PatchOperation::Add { .. } => adds.push(operation.clone()),
169 PatchOperation::Delete { .. } => deletes.push(operation.clone()),
170 PatchOperation::AddGraph { .. } | PatchOperation::DeleteGraph { .. } => {
171 graphs.push(operation.clone())
172 }
173 PatchOperation::TransactionCommit | PatchOperation::TransactionAbort => {
174 tx_end.push(operation.clone())
175 }
176 }
177 }
178
179 patch.operations.clear();
181 patch.operations.extend(headers);
182 patch.operations.extend(prefixes);
183 patch.operations.extend(tx_begin);
184 patch.operations.extend(graphs);
185 patch.operations.extend(deletes); patch.operations.extend(adds);
187 patch.operations.extend(tx_end);
188
189 Ok(patch)
190 }
191
192 fn sort_operations_by_subject(&self, mut patch: RdfPatch) -> Result<RdfPatch> {
193 let ops = &mut patch.operations;
201 let mut i = 0;
202 while i < ops.len() {
203 if Self::is_triple_operation(&ops[i]) {
204 let run_start = i;
205 while i < ops.len() && Self::is_triple_operation(&ops[i]) {
206 i += 1;
207 }
208 ops[run_start..i].sort_by(|a, b| {
211 let key_a = (Self::triple_kind_order(a), Self::extract_subject(a));
212 let key_b = (Self::triple_kind_order(b), Self::extract_subject(b));
213 key_a.cmp(&key_b)
214 });
215 } else {
216 i += 1;
217 }
218 }
219
220 Ok(patch)
221 }
222
223 fn is_triple_operation(operation: &PatchOperation) -> bool {
226 matches!(
227 operation,
228 PatchOperation::Add { .. } | PatchOperation::Delete { .. }
229 )
230 }
231
232 fn triple_kind_order(operation: &PatchOperation) -> u8 {
234 match operation {
235 PatchOperation::Delete { .. } => 0,
236 PatchOperation::Add { .. } => 1,
237 _ => 2,
238 }
239 }
240
241 fn extract_subject(operation: &PatchOperation) -> String {
242 match operation {
243 PatchOperation::Add { subject, .. } | PatchOperation::Delete { subject, .. } => {
244 subject.clone()
245 }
246 _ => String::new(),
247 }
248 }
249}
250
251impl Default for PatchNormalizer {
252 fn default() -> Self {
253 Self::new()
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn regression_normalize_keeps_transaction_commit_last() {
263 let mut patch = RdfPatch::new();
264 patch.add_operation(PatchOperation::TransactionBegin {
265 transaction_id: Some("tx-1".to_string()),
266 });
267 patch.add_operation(PatchOperation::Add {
268 subject: "http://example.org/s1".to_string(),
269 predicate: "http://example.org/p".to_string(),
270 object: "http://example.org/o".to_string(),
271 });
272 patch.add_operation(PatchOperation::TransactionCommit);
273
274 let normalizer = PatchNormalizer::new();
275 let normalized = normalizer.normalize(&patch).unwrap();
276
277 let ops = &normalized.operations;
281 assert!(matches!(
282 ops.first(),
283 Some(PatchOperation::TransactionBegin { .. })
284 ));
285 assert!(matches!(
286 ops.last(),
287 Some(PatchOperation::TransactionCommit)
288 ));
289 let add_pos = ops
290 .iter()
291 .position(|op| matches!(op, PatchOperation::Add { .. }))
292 .expect("Add operation should be present");
293 assert!(add_pos > 0 && add_pos < ops.len() - 1);
294 }
295
296 #[test]
297 fn regression_subject_sort_within_run() {
298 let mut patch = RdfPatch::new();
299 for subject in [
300 "http://example.org/s3",
301 "http://example.org/s1",
302 "http://example.org/s2",
303 ] {
304 patch.add_operation(PatchOperation::Add {
305 subject: subject.to_string(),
306 predicate: "http://example.org/p".to_string(),
307 object: "http://example.org/o".to_string(),
308 });
309 }
310
311 let normalized = PatchNormalizer::new().normalize(&patch).unwrap();
312 let subjects: Vec<String> = normalized
313 .operations
314 .iter()
315 .filter_map(|op| match op {
316 PatchOperation::Add { subject, .. } => Some(subject.clone()),
317 _ => None,
318 })
319 .collect();
320
321 assert_eq!(
322 subjects,
323 vec![
324 "http://example.org/s1".to_string(),
325 "http://example.org/s2".to_string(),
326 "http://example.org/s3".to_string(),
327 ]
328 );
329 }
330}