1use super::PatchResult;
4use crate::{PatchOperation, RdfPatch};
5use anyhow::{anyhow, Result};
6use tracing::debug;
7
8pub struct PatchContext {
9 pub strict_mode: bool,
10 pub validate_operations: bool,
11 pub dry_run: bool,
12}
13
14impl Default for PatchContext {
15 fn default() -> Self {
16 Self {
17 strict_mode: false,
18 validate_operations: true,
19 dry_run: false,
20 }
21 }
22}
23
24pub trait PatchSink {
33 fn add_triple(&mut self, subject: &str, predicate: &str, object: &str) -> Result<()>;
34 fn remove_triple(&mut self, subject: &str, predicate: &str, object: &str) -> Result<()>;
35 fn add_graph(&mut self, graph: &str) -> Result<()>;
36 fn delete_graph(&mut self, graph: &str) -> Result<()>;
37 fn add_prefix(&mut self, prefix: &str, namespace: &str) -> Result<()>;
38 fn remove_prefix(&mut self, prefix: &str) -> Result<()>;
39 fn begin_transaction(&mut self, transaction_id: Option<&str>) -> Result<()>;
40 fn commit_transaction(&mut self) -> Result<()>;
41 fn abort_transaction(&mut self) -> Result<()>;
42 fn header(&mut self, _key: &str, _value: &str) -> Result<()> {
44 Ok(())
45 }
46}
47
48pub fn apply_patch_with_context(patch: &RdfPatch, context: &PatchContext) -> Result<PatchResult> {
57 if !context.dry_run {
58 return Err(anyhow!(
59 "apply_patch_with_context cannot persist changes: no RDF store is wired. \
60 Use apply_patch_to_sink() with a PatchSink, or set context.dry_run \
61 for validation-only processing."
62 ));
63 }
64
65 debug!("Performing dry run / validation of patch {}", patch.id);
66
67 let mut result = PatchResult::new();
68 for operation in patch.operations.iter() {
69 if context.validate_operations {
70 validate_operation(operation)?;
71 }
72 result.operations_applied += 1; }
74
75 result.patch_id = patch.id.clone();
76 result.total_operations = patch.operations.len();
77 Ok(result)
78}
79
80pub fn apply_patch(patch: &RdfPatch) -> Result<PatchResult> {
86 apply_patch_with_context(patch, &PatchContext::default())
87}
88
89pub fn apply_patch_to_sink<S: PatchSink + ?Sized>(
96 patch: &RdfPatch,
97 context: &PatchContext,
98 sink: &mut S,
99) -> Result<PatchResult> {
100 let mut result = PatchResult::new();
101
102 for (i, operation) in patch.operations.iter().enumerate() {
103 if context.validate_operations {
104 validate_operation(operation)?;
105 }
106
107 if context.dry_run {
108 result.operations_applied += 1;
109 continue;
110 }
111
112 match apply_operation(operation, sink) {
113 Ok(_) => {
114 result.operations_applied += 1;
115 debug!("Applied operation {}: {:?}", i, operation);
116 }
117 Err(e) => {
118 result.errors.push(format!("Operation {i}: {e}"));
119 if context.strict_mode {
120 return Err(anyhow!("Failed to apply operation {}: {}", i, e));
121 }
122 }
123 }
124 }
125
126 result.patch_id = patch.id.clone();
127 result.total_operations = patch.operations.len();
128 Ok(result)
129}
130
131fn validate_operation(operation: &PatchOperation) -> Result<()> {
132 match operation {
133 PatchOperation::Add {
134 subject,
135 predicate,
136 object,
137 }
138 | PatchOperation::Delete {
139 subject,
140 predicate,
141 object,
142 } => {
143 if subject.is_empty() || predicate.is_empty() || object.is_empty() {
144 return Err(anyhow!("Triple operation has empty components"));
145 }
146 }
147 PatchOperation::AddGraph { graph } | PatchOperation::DeleteGraph { graph } => {
148 if graph.is_empty() {
149 return Err(anyhow!("Graph operation has empty graph URI"));
150 }
151 }
152 PatchOperation::AddPrefix {
153 prefix: _,
154 namespace: _,
155 } => {
156 }
158 PatchOperation::DeletePrefix { prefix: _ } => {
159 }
161 PatchOperation::TransactionBegin { .. } => {
162 }
164 PatchOperation::TransactionCommit => {
165 }
167 PatchOperation::TransactionAbort => {
168 }
170 PatchOperation::Header { .. } => {
171 }
173 }
174 Ok(())
175}
176
177fn apply_operation<S: PatchSink + ?Sized>(operation: &PatchOperation, sink: &mut S) -> Result<()> {
182 use tracing::warn;
183
184 match operation {
185 PatchOperation::Add {
186 subject,
187 predicate,
188 object,
189 } => {
190 validate_rdf_term(subject, "subject")?;
191 validate_rdf_term(predicate, "predicate")?;
192 validate_rdf_term(object, "object")?;
193 sink.add_triple(subject, predicate, object)?;
194 }
195
196 PatchOperation::Delete {
197 subject,
198 predicate,
199 object,
200 } => {
201 validate_rdf_term(subject, "subject")?;
202 validate_rdf_term(predicate, "predicate")?;
203 validate_rdf_term(object, "object")?;
204 sink.remove_triple(subject, predicate, object)?;
205 }
206
207 PatchOperation::AddGraph { graph } => {
208 validate_rdf_term(graph, "graph")?;
209 sink.add_graph(graph)?;
210 }
211
212 PatchOperation::DeleteGraph { graph } => {
213 validate_rdf_term(graph, "graph")?;
214 sink.delete_graph(graph)?;
215 }
216
217 PatchOperation::AddPrefix { prefix, namespace } => {
218 if prefix.is_empty() {
219 return Err(anyhow!("Prefix name cannot be empty"));
220 }
221 if !namespace.starts_with("http://")
222 && !namespace.starts_with("https://")
223 && !namespace.starts_with("urn:")
224 {
225 warn!(
226 "Namespace '{}' doesn't follow standard URI scheme",
227 namespace
228 );
229 }
230 sink.add_prefix(prefix, namespace)?;
231 }
232
233 PatchOperation::DeletePrefix { prefix } => {
234 if prefix.is_empty() {
235 return Err(anyhow!("Prefix name cannot be empty"));
236 }
237 sink.remove_prefix(prefix)?;
238 }
239
240 PatchOperation::TransactionBegin { transaction_id } => {
241 sink.begin_transaction(transaction_id.as_deref())?;
242 }
243
244 PatchOperation::TransactionCommit => {
245 sink.commit_transaction()?;
246 }
247
248 PatchOperation::TransactionAbort => {
249 sink.abort_transaction()?;
250 }
251
252 PatchOperation::Header { key, value } => {
253 if key == "timestamp" && chrono::DateTime::parse_from_rfc3339(value).is_err() {
254 warn!("Invalid timestamp format in header: {}", value);
255 }
256 sink.header(key, value)?;
257 }
258 }
259
260 Ok(())
261}
262
263fn validate_rdf_term(term: &str, term_type: &str) -> Result<()> {
265 if term.is_empty() {
266 return Err(anyhow!("{} cannot be empty", term_type));
267 }
268
269 if term.starts_with('<') && term.ends_with('>') {
271 let iri = &term[1..term.len() - 1];
272 if iri.is_empty() {
273 return Err(anyhow!("Empty IRI in {}", term_type));
274 }
275
276 if iri.contains(' ') || iri.contains('\n') || iri.contains('\t') {
278 return Err(anyhow!("Invalid characters in IRI: {}", iri));
279 }
280 }
281 else if term.starts_with('_') {
283 if !term.starts_with("_:") {
284 return Err(anyhow!("Invalid blank node format: {}", term));
285 }
286
287 let local_name = &term[2..];
288 if local_name.is_empty() {
289 return Err(anyhow!("Empty blank node local name"));
290 }
291 }
292 else if term.starts_with('"') {
294 if !term.ends_with('"') && !term.contains("\"@") && !term.contains("\"^^") {
295 return Err(anyhow!("Invalid literal format: {}", term));
296 }
297 }
298 else if term.contains(':') {
300 let parts: Vec<&str> = term.splitn(2, ':').collect();
301 if parts.len() != 2 {
302 return Err(anyhow!("Invalid prefixed name format: {}", term));
303 }
304
305 let prefix = parts[0];
306 let local_name = parts[1];
307
308 if prefix.is_empty() && local_name.is_empty() {
310 return Err(anyhow!("Invalid prefixed name: {}", term));
311 }
312 }
313 else if term_type == "predicate" {
315 return Err(anyhow!(
317 "Predicate must be an IRI or prefixed name: {}",
318 term
319 ));
320 }
321
322 Ok(())
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[derive(Default)]
331 struct CollectingSink {
332 triples: Vec<(String, String, String)>,
333 removed: Vec<(String, String, String)>,
334 transactions: Vec<String>,
335 }
336
337 impl PatchSink for CollectingSink {
338 fn add_triple(&mut self, subject: &str, predicate: &str, object: &str) -> Result<()> {
339 self.triples.push((
340 subject.to_string(),
341 predicate.to_string(),
342 object.to_string(),
343 ));
344 Ok(())
345 }
346 fn remove_triple(&mut self, subject: &str, predicate: &str, object: &str) -> Result<()> {
347 self.removed.push((
348 subject.to_string(),
349 predicate.to_string(),
350 object.to_string(),
351 ));
352 Ok(())
353 }
354 fn add_graph(&mut self, _graph: &str) -> Result<()> {
355 Ok(())
356 }
357 fn delete_graph(&mut self, _graph: &str) -> Result<()> {
358 Ok(())
359 }
360 fn add_prefix(&mut self, _prefix: &str, _namespace: &str) -> Result<()> {
361 Ok(())
362 }
363 fn remove_prefix(&mut self, _prefix: &str) -> Result<()> {
364 Ok(())
365 }
366 fn begin_transaction(&mut self, transaction_id: Option<&str>) -> Result<()> {
367 self.transactions
368 .push(format!("begin:{}", transaction_id.unwrap_or("auto")));
369 Ok(())
370 }
371 fn commit_transaction(&mut self) -> Result<()> {
372 self.transactions.push("commit".to_string());
373 Ok(())
374 }
375 fn abort_transaction(&mut self) -> Result<()> {
376 self.transactions.push("abort".to_string());
377 Ok(())
378 }
379 }
380
381 #[test]
382 fn regression_apply_patch_to_sink_actually_mutates() {
383 let mut patch = RdfPatch::new();
384 patch.add_operation(PatchOperation::Add {
385 subject: "http://example.org/s".to_string(),
386 predicate: "http://example.org/p".to_string(),
387 object: "http://example.org/o".to_string(),
388 });
389 patch.add_operation(PatchOperation::Delete {
390 subject: "http://example.org/s".to_string(),
391 predicate: "http://example.org/p".to_string(),
392 object: "http://example.org/old".to_string(),
393 });
394
395 let mut sink = CollectingSink::default();
396 let result = apply_patch_to_sink(&patch, &PatchContext::default(), &mut sink).unwrap();
397
398 assert_eq!(result.operations_applied, 2);
399 assert_eq!(sink.triples.len(), 1);
400 assert_eq!(sink.removed.len(), 1);
401 assert_eq!(sink.triples[0].0, "http://example.org/s");
402 }
403
404 #[test]
405 fn regression_storeless_apply_is_fail_loud() {
406 let mut patch = RdfPatch::new();
407 patch.add_operation(PatchOperation::Add {
408 subject: "http://example.org/s".to_string(),
409 predicate: "http://example.org/p".to_string(),
410 object: "http://example.org/o".to_string(),
411 });
412
413 let ctx = PatchContext {
415 strict_mode: false,
416 validate_operations: true,
417 dry_run: false,
418 };
419 assert!(apply_patch_with_context(&patch, &ctx).is_err());
420
421 let dry = PatchContext {
423 dry_run: true,
424 ..Default::default()
425 };
426 let result = apply_patch_with_context(&patch, &dry).unwrap();
427 assert_eq!(result.operations_applied, 1);
428 }
429}