oxirs_stream/patch/
context.rs

1//! Patch context and application
2
3use 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
24/// Apply RDF Patch operations to a dataset
25pub 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; // Count for dry run
52        }
53    }
54
55    result.patch_id = patch.id.clone();
56    result.total_operations = patch.operations.len();
57
58    Ok(result)
59}
60
61/// Apply RDF Patch operations (convenience function)
62pub 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            // Prefix operations are always valid
92        }
93        PatchOperation::DeletePrefix { prefix: _ } => {
94            // Prefix operations are always valid
95        }
96        PatchOperation::TransactionBegin { .. } => {
97            // Transaction operations are always valid
98        }
99        PatchOperation::TransactionCommit => {
100            // Transaction operations are always valid
101        }
102        PatchOperation::TransactionAbort => {
103            // Transaction operations are always valid
104        }
105        PatchOperation::Header { .. } => {
106            // Header operations are always valid
107        }
108    }
109    Ok(())
110}
111
112/// Apply a patch operation to an RDF store
113///
114/// In a production system, this would integrate with oxirs-core's RDF store.
115/// For now, this provides a realistic implementation that logs operations
116/// and performs validation checks.
117fn 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 the triple components
132            validate_rdf_term(subject, "subject")?;
133            validate_rdf_term(predicate, "predicate")?;
134            validate_rdf_term(object, "object")?;
135
136            // In a real implementation, this would call:
137            // store.add_triple(subject, predicate, object)?;
138
139            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 the triple components
153            validate_rdf_term(subject, "subject")?;
154            validate_rdf_term(predicate, "predicate")?;
155            validate_rdf_term(object, "object")?;
156
157            // In a real implementation, this would call:
158            // store.remove_triple(subject, predicate, object)?;
159
160            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            // In a real implementation, this would call:
169            // store.create_graph(graph)?;
170
171            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            // In a real implementation, this would call:
180            // store.drop_graph(graph)?;
181
182            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            // In a real implementation, this would call:
206            // store.add_prefix(prefix, namespace)?;
207
208            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            // In a real implementation, this would call:
219            // store.remove_prefix(prefix)?;
220
221            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            // In a real implementation, this would call:
232            // store.begin_transaction(transaction_id.as_deref())?;
233
234            debug!("Successfully started transaction");
235        }
236
237        PatchOperation::TransactionCommit => {
238            info!("Applying TRANSACTION COMMIT");
239
240            // In a real implementation, this would call:
241            // store.commit_transaction()?;
242
243            debug!("Successfully committed transaction");
244        }
245
246        PatchOperation::TransactionAbort => {
247            info!("Applying TRANSACTION ABORT");
248
249            // In a real implementation, this would call:
250            // store.abort_transaction()?;
251
252            debug!("Successfully aborted transaction");
253        }
254
255        PatchOperation::Header { key, value } => {
256            debug!("Processing header: {} = {}", key, value);
257
258            // Headers are metadata and don't modify the store
259            // They might be used for patch provenance, timestamps, etc.
260
261            match key.as_str() {
262                "timestamp" => {
263                    // Validate timestamp format
264                    if chrono::DateTime::parse_from_rfc3339(value).is_err() {
265                        warn!("Invalid timestamp format in header: {}", value);
266                    }
267                }
268                "creator" | "description" => {
269                    // Informational headers - no validation needed
270                }
271                _ => {
272                    debug!("Unknown header type: {}", key);
273                }
274            }
275        }
276    }
277
278    Ok(())
279}
280
281/// Validate an RDF term (IRI, blank node, or literal)
282fn 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    // Check for IRI format
288    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        // Basic IRI validation - should contain valid characters
295        if iri.contains(' ') || iri.contains('\n') || iri.contains('\t') {
296            return Err(anyhow!("Invalid characters in IRI: {}", iri));
297        }
298    }
299    // Check for blank node format
300    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    // Check for literal format (quoted strings)
311    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    // Check for prefixed name
317    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        // Prefix should not be empty (unless it's the default prefix)
327        if prefix.is_empty() && local_name.is_empty() {
328            return Err(anyhow!("Invalid prefixed name: {}", term));
329        }
330    }
331    // If none of the above, it might be a relative IRI or invalid
332    else if term_type == "predicate" {
333        // Predicates should always be IRIs or prefixed names
334        return Err(anyhow!(
335            "Predicate must be an IRI or prefixed name: {}",
336            term
337        ));
338    }
339
340    Ok(())
341}
342