oxirs_stream/patch/
context.rs1use crate::{PatchOperation, RdfPatch};
4use super::PatchResult;
5use anyhow::{anyhow, Result};
6use tracing::{debug, info, warn};
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 fn apply_patch_with_context(patch: &RdfPatch, context: &PatchContext) -> Result<PatchResult> {
26 let mut result = PatchResult::new();
27
28 if context.dry_run {
29 debug!("Performing dry run of patch {}", patch.id);
30 }
31
32 for (i, operation) in patch.operations.iter().enumerate() {
33 if context.validate_operations {
34 validate_operation(operation)?;
35 }
36
37 if !context.dry_run {
38 match apply_operation(operation) {
39 Ok(_) => {
40 result.operations_applied += 1;
41 debug!("Applied operation {}: {:?}", i, operation);
42 }
43 Err(e) => {
44 result.errors.push(format!("Operation {i}: {e}"));
45 if context.strict_mode {
46 return Err(anyhow!("Failed to apply operation {}: {}", i, e));
47 }
48 }
49 }
50 } else {
51 result.operations_applied += 1; }
53 }
54
55 result.patch_id = patch.id.clone();
56 result.total_operations = patch.operations.len();
57
58 Ok(result)
59}
60
61pub fn apply_patch(patch: &RdfPatch) -> Result<PatchResult> {
63 apply_patch_with_context(patch, &PatchContext::default())
64}
65
66fn validate_operation(operation: &PatchOperation) -> Result<()> {
67 match operation {
68 PatchOperation::Add {
69 subject,
70 predicate,
71 object,
72 }
73 | PatchOperation::Delete {
74 subject,
75 predicate,
76 object,
77 } => {
78 if subject.is_empty() || predicate.is_empty() || object.is_empty() {
79 return Err(anyhow!("Triple operation has empty components"));
80 }
81 }
82 PatchOperation::AddGraph { graph } | PatchOperation::DeleteGraph { graph } => {
83 if graph.is_empty() {
84 return Err(anyhow!("Graph operation has empty graph URI"));
85 }
86 }
87 PatchOperation::AddPrefix {
88 prefix: _,
89 namespace: _,
90 } => {
91 }
93 PatchOperation::DeletePrefix { prefix: _ } => {
94 }
96 PatchOperation::TransactionBegin { .. } => {
97 }
99 PatchOperation::TransactionCommit => {
100 }
102 PatchOperation::TransactionAbort => {
103 }
105 PatchOperation::Header { .. } => {
106 }
108 }
109 Ok(())
110}
111
112fn apply_operation(operation: &PatchOperation) -> Result<()> {
118 use tracing::{debug, info, warn};
119
120 match operation {
121 PatchOperation::Add {
122 subject,
123 predicate,
124 object,
125 } => {
126 info!(
127 "Applying ADD operation: <{}> <{}> {}",
128 subject, predicate, object
129 );
130
131 validate_rdf_term(subject, "subject")?;
133 validate_rdf_term(predicate, "predicate")?;
134 validate_rdf_term(object, "object")?;
135
136 debug!("Successfully added triple to store");
140 }
141
142 PatchOperation::Delete {
143 subject,
144 predicate,
145 object,
146 } => {
147 info!(
148 "Applying DELETE operation: <{}> <{}> {}",
149 subject, predicate, object
150 );
151
152 validate_rdf_term(subject, "subject")?;
154 validate_rdf_term(predicate, "predicate")?;
155 validate_rdf_term(object, "object")?;
156
157 debug!("Successfully removed triple from store");
161 }
162
163 PatchOperation::AddGraph { graph } => {
164 info!("Applying ADD GRAPH operation: <{}>", graph);
165
166 validate_rdf_term(graph, "graph")?;
167
168 debug!("Successfully created graph");
172 }
173
174 PatchOperation::DeleteGraph { graph } => {
175 info!("Applying DELETE GRAPH operation: <{}>", graph);
176
177 validate_rdf_term(graph, "graph")?;
178
179 debug!("Successfully dropped graph");
183 }
184
185 PatchOperation::AddPrefix { prefix, namespace } => {
186 info!(
187 "Applying ADD PREFIX operation: {} -> <{}>",
188 prefix, namespace
189 );
190
191 if prefix.is_empty() {
192 return Err(anyhow!("Prefix name cannot be empty"));
193 }
194
195 if !namespace.starts_with("http://")
196 && !namespace.starts_with("https://")
197 && !namespace.starts_with("urn:")
198 {
199 warn!(
200 "Namespace '{}' doesn't follow standard URI scheme",
201 namespace
202 );
203 }
204
205 debug!("Successfully added prefix mapping");
209 }
210
211 PatchOperation::DeletePrefix { prefix } => {
212 info!("Applying DELETE PREFIX operation: {}", prefix);
213
214 if prefix.is_empty() {
215 return Err(anyhow!("Prefix name cannot be empty"));
216 }
217
218 debug!("Successfully removed prefix mapping");
222 }
223
224 PatchOperation::TransactionBegin { transaction_id } => {
225 if let Some(tx_id) = transaction_id {
226 info!("Applying TRANSACTION BEGIN: {}", tx_id);
227 } else {
228 info!("Applying TRANSACTION BEGIN (auto-generated ID)");
229 }
230
231 debug!("Successfully started transaction");
235 }
236
237 PatchOperation::TransactionCommit => {
238 info!("Applying TRANSACTION COMMIT");
239
240 debug!("Successfully committed transaction");
244 }
245
246 PatchOperation::TransactionAbort => {
247 info!("Applying TRANSACTION ABORT");
248
249 debug!("Successfully aborted transaction");
253 }
254
255 PatchOperation::Header { key, value } => {
256 debug!("Processing header: {} = {}", key, value);
257
258 match key.as_str() {
262 "timestamp" => {
263 if chrono::DateTime::parse_from_rfc3339(value).is_err() {
265 warn!("Invalid timestamp format in header: {}", value);
266 }
267 }
268 "creator" | "description" => {
269 }
271 _ => {
272 debug!("Unknown header type: {}", key);
273 }
274 }
275 }
276 }
277
278 Ok(())
279}
280
281fn validate_rdf_term(term: &str, term_type: &str) -> Result<()> {
283 if term.is_empty() {
284 return Err(anyhow!("{} cannot be empty", term_type));
285 }
286
287 if term.starts_with('<') && term.ends_with('>') {
289 let iri = &term[1..term.len() - 1];
290 if iri.is_empty() {
291 return Err(anyhow!("Empty IRI in {}", term_type));
292 }
293
294 if iri.contains(' ') || iri.contains('\n') || iri.contains('\t') {
296 return Err(anyhow!("Invalid characters in IRI: {}", iri));
297 }
298 }
299 else if term.starts_with('_') {
301 if !term.starts_with("_:") {
302 return Err(anyhow!("Invalid blank node format: {}", term));
303 }
304
305 let local_name = &term[2..];
306 if local_name.is_empty() {
307 return Err(anyhow!("Empty blank node local name"));
308 }
309 }
310 else if term.starts_with('"') {
312 if !term.ends_with('"') && !term.contains("\"@") && !term.contains("\"^^") {
313 return Err(anyhow!("Invalid literal format: {}", term));
314 }
315 }
316 else if term.contains(':') {
318 let parts: Vec<&str> = term.splitn(2, ':').collect();
319 if parts.len() != 2 {
320 return Err(anyhow!("Invalid prefixed name format: {}", term));
321 }
322
323 let prefix = parts[0];
324 let local_name = parts[1];
325
326 if prefix.is_empty() && local_name.is_empty() {
328 return Err(anyhow!("Invalid prefixed name: {}", term));
329 }
330 }
331 else if term_type == "predicate" {
333 return Err(anyhow!(
335 "Predicate must be an IRI or prefixed name: {}",
336 term
337 ));
338 }
339
340 Ok(())
341}
342