Skip to main content

strixonomy_edit/
invert.rs

1use crate::change::SemanticChange;
2use crate::error::{EditError, Result};
3use strixonomy_obo::OboPatchOp;
4use strixonomy_owl::PatchOp;
5
6pub fn invert_patch_op(op: &PatchOp) -> Result<PatchOp> {
7    Ok(match op {
8        PatchOp::AddPrefix { prefix, namespace_iri: _ } => {
9            PatchOp::RemovePrefix { prefix: prefix.clone() }
10        }
11        PatchOp::RemovePrefix { .. } => {
12            return Err(EditError::NotInvertible(
13                "remove_prefix inverse requires prior namespace IRI".into(),
14            ));
15        }
16        PatchOp::SetPrefix { .. } => {
17            return Err(EditError::NotInvertible("set_prefix requires prior value".into()));
18        }
19        PatchOp::SetOntologyIri { .. } | PatchOp::SetVersionIri { .. } => {
20            return Err(EditError::NotInvertible("ontology IRI set requires prior value".into()));
21        }
22        PatchOp::AddOntologyAnnotation { ontology_iri, predicate, value } => {
23            PatchOp::RemoveOntologyAnnotation {
24                ontology_iri: ontology_iri.clone(),
25                predicate: predicate.clone(),
26                value: value.clone(),
27            }
28        }
29        PatchOp::RemoveOntologyAnnotation { ontology_iri, predicate, value } => {
30            PatchOp::AddOntologyAnnotation {
31                ontology_iri: ontology_iri.clone(),
32                predicate: predicate.clone(),
33                value: value.clone(),
34            }
35        }
36        PatchOp::CreateEntity { entity_iri, kind: _ } => {
37            PatchOp::DeleteEntity { entity_iri: entity_iri.clone() }
38        }
39        PatchOp::DeleteEntity { entity_iri: _ } => {
40            return Err(EditError::NotInvertible(
41                "delete_entity inverse requires entity kind".into(),
42            ));
43        }
44        PatchOp::SetLabel { .. }
45        | PatchOp::SetComment { .. }
46        | PatchOp::SetEquivalentClass { .. } => {
47            return Err(EditError::NotInvertible("set_* requires prior value".into()));
48        }
49        PatchOp::AddLabel { entity_iri, value } => {
50            PatchOp::RemoveLabel { entity_iri: entity_iri.clone(), value: value.clone() }
51        }
52        PatchOp::RemoveLabel { entity_iri, value } => {
53            PatchOp::AddLabel { entity_iri: entity_iri.clone(), value: value.clone() }
54        }
55        PatchOp::AddComment { entity_iri, value } => {
56            PatchOp::RemoveComment { entity_iri: entity_iri.clone(), value: value.clone() }
57        }
58        PatchOp::RemoveComment { entity_iri, value } => {
59            PatchOp::AddComment { entity_iri: entity_iri.clone(), value: value.clone() }
60        }
61        PatchOp::AddSubClassOf { entity_iri, parent_iri } => PatchOp::RemoveSubClassOf {
62            entity_iri: entity_iri.clone(),
63            parent_iri: parent_iri.clone(),
64        },
65        PatchOp::RemoveSubClassOf { entity_iri, parent_iri } => PatchOp::AddSubClassOf {
66            entity_iri: entity_iri.clone(),
67            parent_iri: parent_iri.clone(),
68        },
69        PatchOp::AddComplexSubClassOf { entity_iri, manchester } => {
70            PatchOp::RemoveComplexSubClassOf {
71                entity_iri: entity_iri.clone(),
72                manchester: manchester.clone(),
73            }
74        }
75        PatchOp::RemoveComplexSubClassOf { entity_iri, manchester } => {
76            PatchOp::AddComplexSubClassOf {
77                entity_iri: entity_iri.clone(),
78                manchester: manchester.clone(),
79            }
80        }
81        PatchOp::AddEquivalentClass { entity_iri, manchester } => PatchOp::RemoveEquivalentClass {
82            entity_iri: entity_iri.clone(),
83            manchester: manchester.clone(),
84        },
85        PatchOp::RemoveEquivalentClass { entity_iri, manchester } => PatchOp::AddEquivalentClass {
86            entity_iri: entity_iri.clone(),
87            manchester: manchester.clone(),
88        },
89        PatchOp::SetDeprecated { entity_iri, value } => {
90            if !*value {
91                return Err(EditError::NotInvertible(
92                    "set_deprecated false inverse would invent deprecation without prior state"
93                        .into(),
94                ));
95            }
96            PatchOp::SetDeprecated { entity_iri: entity_iri.clone(), value: false }
97        }
98        PatchOp::AddDisjointClass { entity_iri, other_iri } => PatchOp::RemoveDisjointClass {
99            entity_iri: entity_iri.clone(),
100            other_iri: other_iri.clone(),
101        },
102        PatchOp::RemoveDisjointClass { entity_iri, other_iri } => PatchOp::AddDisjointClass {
103            entity_iri: entity_iri.clone(),
104            other_iri: other_iri.clone(),
105        },
106        PatchOp::AddImport { ontology_iri, import_iri } => PatchOp::RemoveImport {
107            ontology_iri: ontology_iri.clone(),
108            import_iri: import_iri.clone(),
109        },
110        PatchOp::RemoveImport { ontology_iri, import_iri } => PatchOp::AddImport {
111            ontology_iri: ontology_iri.clone(),
112            import_iri: import_iri.clone(),
113        },
114        PatchOp::AddDomain { entity_iri, class_iri } => {
115            PatchOp::RemoveDomain { entity_iri: entity_iri.clone(), class_iri: class_iri.clone() }
116        }
117        PatchOp::RemoveDomain { entity_iri, class_iri } => {
118            PatchOp::AddDomain { entity_iri: entity_iri.clone(), class_iri: class_iri.clone() }
119        }
120        PatchOp::AddRange { entity_iri, range_iri } => {
121            PatchOp::RemoveRange { entity_iri: entity_iri.clone(), range_iri: range_iri.clone() }
122        }
123        PatchOp::RemoveRange { entity_iri, range_iri } => {
124            PatchOp::AddRange { entity_iri: entity_iri.clone(), range_iri: range_iri.clone() }
125        }
126        PatchOp::SetFunctional { .. }
127        | PatchOp::SetInverseFunctional { .. }
128        | PatchOp::SetTransitive { .. }
129        | PatchOp::SetSymmetric { .. }
130        | PatchOp::SetAsymmetric { .. }
131        | PatchOp::SetReflexive { .. }
132        | PatchOp::SetIrreflexive { .. } => {
133            let value = match op {
134                PatchOp::SetFunctional { value, .. }
135                | PatchOp::SetInverseFunctional { value, .. }
136                | PatchOp::SetTransitive { value, .. }
137                | PatchOp::SetSymmetric { value, .. }
138                | PatchOp::SetAsymmetric { value, .. }
139                | PatchOp::SetReflexive { value, .. }
140                | PatchOp::SetIrreflexive { value, .. } => *value,
141                _ => unreachable!(),
142            };
143            if !value {
144                return Err(EditError::NotInvertible(
145                    "clearing a property characteristic is not invertible without prior state"
146                        .into(),
147                ));
148            }
149            match op {
150                PatchOp::SetFunctional { entity_iri, .. } => {
151                    PatchOp::SetFunctional { entity_iri: entity_iri.clone(), value: false }
152                }
153                PatchOp::SetInverseFunctional { entity_iri, .. } => {
154                    PatchOp::SetInverseFunctional { entity_iri: entity_iri.clone(), value: false }
155                }
156                PatchOp::SetTransitive { entity_iri, .. } => {
157                    PatchOp::SetTransitive { entity_iri: entity_iri.clone(), value: false }
158                }
159                PatchOp::SetSymmetric { entity_iri, .. } => {
160                    PatchOp::SetSymmetric { entity_iri: entity_iri.clone(), value: false }
161                }
162                PatchOp::SetAsymmetric { entity_iri, .. } => {
163                    PatchOp::SetAsymmetric { entity_iri: entity_iri.clone(), value: false }
164                }
165                PatchOp::SetReflexive { entity_iri, .. } => {
166                    PatchOp::SetReflexive { entity_iri: entity_iri.clone(), value: false }
167                }
168                PatchOp::SetIrreflexive { entity_iri, .. } => {
169                    PatchOp::SetIrreflexive { entity_iri: entity_iri.clone(), value: false }
170                }
171                _ => unreachable!(),
172            }
173        }
174        PatchOp::AddPropertyChain { entity_iri, properties } => PatchOp::RemovePropertyChain {
175            entity_iri: entity_iri.clone(),
176            properties: properties.clone(),
177        },
178        PatchOp::RemovePropertyChain { entity_iri, properties } => PatchOp::AddPropertyChain {
179            entity_iri: entity_iri.clone(),
180            properties: properties.clone(),
181        },
182        PatchOp::AddClassAssertion { entity_iri, class_iri } => PatchOp::RemoveClassAssertion {
183            entity_iri: entity_iri.clone(),
184            class_iri: class_iri.clone(),
185        },
186        PatchOp::RemoveClassAssertion { entity_iri, class_iri } => PatchOp::AddClassAssertion {
187            entity_iri: entity_iri.clone(),
188            class_iri: class_iri.clone(),
189        },
190        PatchOp::AddObjectPropertyAssertion { entity_iri, property_iri, target_iri } => {
191            PatchOp::RemoveObjectPropertyAssertion {
192                entity_iri: entity_iri.clone(),
193                property_iri: property_iri.clone(),
194                target_iri: target_iri.clone(),
195            }
196        }
197        PatchOp::RemoveObjectPropertyAssertion { entity_iri, property_iri, target_iri } => {
198            PatchOp::AddObjectPropertyAssertion {
199                entity_iri: entity_iri.clone(),
200                property_iri: property_iri.clone(),
201                target_iri: target_iri.clone(),
202            }
203        }
204        PatchOp::AddDataPropertyAssertion { entity_iri, property_iri, value } => {
205            PatchOp::RemoveDataPropertyAssertion {
206                entity_iri: entity_iri.clone(),
207                property_iri: property_iri.clone(),
208                value: value.clone(),
209            }
210        }
211        PatchOp::RemoveDataPropertyAssertion { entity_iri, property_iri, value } => {
212            PatchOp::AddDataPropertyAssertion {
213                entity_iri: entity_iri.clone(),
214                property_iri: property_iri.clone(),
215                value: value.clone(),
216            }
217        }
218        PatchOp::AddAnnotation { entity_iri, predicate, value } => PatchOp::RemoveAnnotation {
219            entity_iri: entity_iri.clone(),
220            predicate: predicate.clone(),
221            value: value.clone(),
222        },
223        PatchOp::RemoveAnnotation { entity_iri, predicate, value } => PatchOp::AddAnnotation {
224            entity_iri: entity_iri.clone(),
225            predicate: predicate.clone(),
226            value: value.clone(),
227        },
228        PatchOp::AddHasKey { class_iri, properties } => {
229            PatchOp::RemoveHasKey { class_iri: class_iri.clone(), properties: properties.clone() }
230        }
231        PatchOp::RemoveHasKey { class_iri, properties } => {
232            PatchOp::AddHasKey { class_iri: class_iri.clone(), properties: properties.clone() }
233        }
234        PatchOp::AddDisjointUnion { class_iri, members } => {
235            PatchOp::RemoveDisjointUnion { class_iri: class_iri.clone(), members: members.clone() }
236        }
237        PatchOp::RemoveDisjointUnion { class_iri, members } => {
238            PatchOp::AddDisjointUnion { class_iri: class_iri.clone(), members: members.clone() }
239        }
240        PatchOp::AddInverseObjectProperties { property_iri, inverse_iri } => {
241            PatchOp::RemoveInverseObjectProperties {
242                property_iri: property_iri.clone(),
243                inverse_iri: inverse_iri.clone(),
244            }
245        }
246        PatchOp::RemoveInverseObjectProperties { property_iri, inverse_iri } => {
247            PatchOp::AddInverseObjectProperties {
248                property_iri: property_iri.clone(),
249                inverse_iri: inverse_iri.clone(),
250            }
251        }
252        PatchOp::AddEquivalentObjectProperties { properties } => {
253            PatchOp::RemoveEquivalentObjectProperties { properties: properties.clone() }
254        }
255        PatchOp::RemoveEquivalentObjectProperties { properties } => {
256            PatchOp::AddEquivalentObjectProperties { properties: properties.clone() }
257        }
258        PatchOp::AddDisjointObjectProperties { properties } => {
259            PatchOp::RemoveDisjointObjectProperties { properties: properties.clone() }
260        }
261        PatchOp::RemoveDisjointObjectProperties { properties } => {
262            PatchOp::AddDisjointObjectProperties { properties: properties.clone() }
263        }
264        PatchOp::AddEquivalentDataProperties { properties } => {
265            PatchOp::RemoveEquivalentDataProperties { properties: properties.clone() }
266        }
267        PatchOp::RemoveEquivalentDataProperties { properties } => {
268            PatchOp::AddEquivalentDataProperties { properties: properties.clone() }
269        }
270        PatchOp::AddDisjointDataProperties { properties } => {
271            PatchOp::RemoveDisjointDataProperties { properties: properties.clone() }
272        }
273        PatchOp::RemoveDisjointDataProperties { properties } => {
274            PatchOp::AddDisjointDataProperties { properties: properties.clone() }
275        }
276        PatchOp::AddSubObjectPropertyOf { property_iri, parent_iri } => {
277            PatchOp::RemoveSubObjectPropertyOf {
278                property_iri: property_iri.clone(),
279                parent_iri: parent_iri.clone(),
280            }
281        }
282        PatchOp::RemoveSubObjectPropertyOf { property_iri, parent_iri } => {
283            PatchOp::AddSubObjectPropertyOf {
284                property_iri: property_iri.clone(),
285                parent_iri: parent_iri.clone(),
286            }
287        }
288        PatchOp::AddSubDataPropertyOf { property_iri, parent_iri } => {
289            PatchOp::RemoveSubDataPropertyOf {
290                property_iri: property_iri.clone(),
291                parent_iri: parent_iri.clone(),
292            }
293        }
294        PatchOp::RemoveSubDataPropertyOf { property_iri, parent_iri } => {
295            PatchOp::AddSubDataPropertyOf {
296                property_iri: property_iri.clone(),
297                parent_iri: parent_iri.clone(),
298            }
299        }
300        PatchOp::AddNegativeObjectPropertyAssertion { entity_iri, property_iri, target_iri } => {
301            PatchOp::RemoveNegativeObjectPropertyAssertion {
302                entity_iri: entity_iri.clone(),
303                property_iri: property_iri.clone(),
304                target_iri: target_iri.clone(),
305            }
306        }
307        PatchOp::RemoveNegativeObjectPropertyAssertion { entity_iri, property_iri, target_iri } => {
308            PatchOp::AddNegativeObjectPropertyAssertion {
309                entity_iri: entity_iri.clone(),
310                property_iri: property_iri.clone(),
311                target_iri: target_iri.clone(),
312            }
313        }
314        PatchOp::AddNegativeDataPropertyAssertion { entity_iri, property_iri, value } => {
315            PatchOp::RemoveNegativeDataPropertyAssertion {
316                entity_iri: entity_iri.clone(),
317                property_iri: property_iri.clone(),
318                value: value.clone(),
319            }
320        }
321        PatchOp::RemoveNegativeDataPropertyAssertion { entity_iri, property_iri, value } => {
322            PatchOp::AddNegativeDataPropertyAssertion {
323                entity_iri: entity_iri.clone(),
324                property_iri: property_iri.clone(),
325                value: value.clone(),
326            }
327        }
328        PatchOp::AddSameIndividual { individuals } => {
329            PatchOp::RemoveSameIndividual { individuals: individuals.clone() }
330        }
331        PatchOp::RemoveSameIndividual { individuals } => {
332            PatchOp::AddSameIndividual { individuals: individuals.clone() }
333        }
334        PatchOp::AddDifferentIndividuals { individuals } => {
335            PatchOp::RemoveDifferentIndividuals { individuals: individuals.clone() }
336        }
337        PatchOp::RemoveDifferentIndividuals { individuals } => {
338            PatchOp::AddDifferentIndividuals { individuals: individuals.clone() }
339        }
340        PatchOp::AddDatatypeDefinition { datatype_iri, manchester } => {
341            PatchOp::RemoveDatatypeDefinition {
342                datatype_iri: datatype_iri.clone(),
343                manchester: manchester.clone(),
344            }
345        }
346        PatchOp::RemoveDatatypeDefinition { datatype_iri, manchester } => {
347            PatchOp::AddDatatypeDefinition {
348                datatype_iri: datatype_iri.clone(),
349                manchester: manchester.clone(),
350            }
351        }
352        PatchOp::AddAxiomAnnotation { axiom_op, subject_iri, related_iri, predicate, value } => {
353            PatchOp::RemoveAxiomAnnotation {
354                axiom_op: axiom_op.clone(),
355                subject_iri: subject_iri.clone(),
356                related_iri: related_iri.clone(),
357                predicate: predicate.clone(),
358                value: value.clone(),
359            }
360        }
361        PatchOp::RemoveAxiomAnnotation { axiom_op, subject_iri, related_iri, predicate, value } => {
362            PatchOp::AddAxiomAnnotation {
363                axiom_op: axiom_op.clone(),
364                subject_iri: subject_iri.clone(),
365                related_iri: related_iri.clone(),
366                predicate: predicate.clone(),
367                value: value.clone(),
368            }
369        }
370        PatchOp::AddSwrlRule { ontology_iri, rule_json } => PatchOp::RemoveSwrlRule {
371            ontology_iri: ontology_iri.clone(),
372            rule_json: rule_json.clone(),
373        },
374        PatchOp::RemoveSwrlRule { ontology_iri, rule_json } => PatchOp::AddSwrlRule {
375            ontology_iri: ontology_iri.clone(),
376            rule_json: rule_json.clone(),
377        },
378        PatchOp::ReplaceSwrlRule { ontology_iri, old_rule_json, new_rule_json } => {
379            PatchOp::ReplaceSwrlRule {
380                ontology_iri: ontology_iri.clone(),
381                old_rule_json: new_rule_json.clone(),
382                new_rule_json: old_rule_json.clone(),
383            }
384        }
385    })
386}
387
388pub fn invert_obo_patch_op(op: &OboPatchOp) -> Result<OboPatchOp> {
389    Ok(match op {
390        OboPatchOp::SetName { .. } | OboPatchOp::SetNamespace { .. } => {
391            return Err(EditError::NotInvertible("obo set_* requires prior value".into()));
392        }
393        OboPatchOp::AddSynonym { term_id, value, scope } => OboPatchOp::RemoveSynonym {
394            term_id: term_id.clone(),
395            value: value.clone(),
396            scope: Some(scope.clone()),
397        },
398        OboPatchOp::RemoveSynonym { term_id, value, scope: Some(scope) } => {
399            OboPatchOp::AddSynonym {
400                term_id: term_id.clone(),
401                value: value.clone(),
402                scope: scope.clone(),
403            }
404        }
405        OboPatchOp::RemoveSynonym { scope: None, .. } => {
406            return Err(EditError::NotInvertible(
407                "remove_synonym inverse requires recorded prior scope".into(),
408            ));
409        }
410        OboPatchOp::AddDef { .. } => {
411            return Err(EditError::NotInvertible(
412                "add_def may replace a prior def; inverse requires prior value".into(),
413            ));
414        }
415        OboPatchOp::RemoveDef { term_id: _ } => {
416            return Err(EditError::NotInvertible("remove_def inverse requires prior def".into()));
417        }
418        OboPatchOp::AddXref { term_id, xref } => {
419            OboPatchOp::RemoveXref { term_id: term_id.clone(), xref: xref.clone() }
420        }
421        OboPatchOp::RemoveXref { term_id, xref } => {
422            OboPatchOp::AddXref { term_id: term_id.clone(), xref: xref.clone() }
423        }
424        OboPatchOp::SetDeprecated { term_id, value } => {
425            if !*value {
426                return Err(EditError::NotInvertible(
427                    "set_deprecated false inverse would invent obsolescence without prior state"
428                        .into(),
429                ));
430            }
431            OboPatchOp::SetDeprecated { term_id: term_id.clone(), value: false }
432        }
433        OboPatchOp::AddIsA { term_id, parent_id } => {
434            OboPatchOp::RemoveIsA { term_id: term_id.clone(), parent_id: parent_id.clone() }
435        }
436        OboPatchOp::RemoveIsA { term_id, parent_id } => {
437            OboPatchOp::AddIsA { term_id: term_id.clone(), parent_id: parent_id.clone() }
438        }
439    })
440}
441
442pub fn invert_change(change: &SemanticChange) -> Result<SemanticChange> {
443    Ok(match change {
444        SemanticChange::Turtle { change } => {
445            SemanticChange::Turtle { change: invert_patch_op(change)? }
446        }
447        SemanticChange::Obo { change } => {
448            SemanticChange::Obo { change: invert_obo_patch_op(change)? }
449        }
450    })
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn remove_prefix_is_not_invertible() {
459        let err = invert_patch_op(&PatchOp::RemovePrefix { prefix: "ex".into() }).unwrap_err();
460        assert!(matches!(err, EditError::NotInvertible(_)));
461    }
462
463    #[test]
464    fn add_synonym_inverts_to_scoped_remove() {
465        let inverted = invert_obo_patch_op(&OboPatchOp::AddSynonym {
466            term_id: "EX:1".into(),
467            value: "alias".into(),
468            scope: "RELATED".into(),
469        })
470        .expect("invert");
471        assert_eq!(
472            inverted,
473            OboPatchOp::RemoveSynonym {
474                term_id: "EX:1".into(),
475                value: "alias".into(),
476                scope: Some("RELATED".into()),
477            }
478        );
479    }
480
481    #[test]
482    fn remove_synonym_with_scope_inverts_to_add() {
483        let inverted = invert_obo_patch_op(&OboPatchOp::RemoveSynonym {
484            term_id: "EX:1".into(),
485            value: "alias".into(),
486            scope: Some("RELATED".into()),
487        })
488        .expect("invert");
489        assert_eq!(
490            inverted,
491            OboPatchOp::AddSynonym {
492                term_id: "EX:1".into(),
493                value: "alias".into(),
494                scope: "RELATED".into(),
495            }
496        );
497    }
498
499    #[test]
500    fn remove_synonym_without_scope_and_add_def_are_not_invertible() {
501        assert!(matches!(
502            invert_obo_patch_op(&OboPatchOp::RemoveSynonym {
503                term_id: "EX:1".into(),
504                value: "alias".into(),
505                scope: None,
506            }),
507            Err(EditError::NotInvertible(_))
508        ));
509        assert!(matches!(
510            invert_obo_patch_op(&OboPatchOp::AddDef {
511                term_id: "EX:1".into(),
512                value: "new".into(),
513            }),
514            Err(EditError::NotInvertible(_))
515        ));
516    }
517
518    #[test]
519    fn set_deprecated_false_is_not_invertible() {
520        assert!(matches!(
521            invert_patch_op(&PatchOp::SetDeprecated {
522                entity_iri: "http://example.org/A".into(),
523                value: false,
524            }),
525            Err(EditError::NotInvertible(_))
526        ));
527        assert!(matches!(
528            invert_obo_patch_op(&OboPatchOp::SetDeprecated {
529                term_id: "EX:1".into(),
530                value: false,
531            }),
532            Err(EditError::NotInvertible(_))
533        ));
534    }
535
536    #[test]
537    fn set_deprecated_true_inverts_to_false() {
538        let inverted = invert_patch_op(&PatchOp::SetDeprecated {
539            entity_iri: "http://example.org/A".into(),
540            value: true,
541        })
542        .expect("invert");
543        assert_eq!(
544            inverted,
545            PatchOp::SetDeprecated { entity_iri: "http://example.org/A".into(), value: false }
546        );
547    }
548
549    #[test]
550    fn clearing_boolean_characteristic_is_not_invertible() {
551        assert!(matches!(
552            invert_patch_op(&PatchOp::SetFunctional {
553                entity_iri: "http://example.org/p".into(),
554                value: false,
555            }),
556            Err(EditError::NotInvertible(_))
557        ));
558        let inverted = invert_patch_op(&PatchOp::SetFunctional {
559            entity_iri: "http://example.org/p".into(),
560            value: true,
561        })
562        .expect("invert set true");
563        assert_eq!(
564            inverted,
565            PatchOp::SetFunctional { entity_iri: "http://example.org/p".into(), value: false }
566        );
567    }
568
569    #[test]
570    fn has_key_and_inverse_invert() {
571        let inverted = invert_patch_op(&PatchOp::AddHasKey {
572            class_iri: "http://example.org/C".into(),
573            properties: vec!["http://example.org/p".into()],
574        })
575        .expect("invert has key");
576        assert_eq!(
577            inverted,
578            PatchOp::RemoveHasKey {
579                class_iri: "http://example.org/C".into(),
580                properties: vec!["http://example.org/p".into()],
581            }
582        );
583        let inverted = invert_patch_op(&PatchOp::AddInverseObjectProperties {
584            property_iri: "http://example.org/p".into(),
585            inverse_iri: "http://example.org/q".into(),
586        })
587        .expect("invert inverse");
588        assert_eq!(
589            inverted,
590            PatchOp::RemoveInverseObjectProperties {
591                property_iri: "http://example.org/p".into(),
592                inverse_iri: "http://example.org/q".into(),
593            }
594        );
595    }
596
597    #[test]
598    fn negative_assertion_and_same_individual_invert() {
599        let inverted = invert_patch_op(&PatchOp::AddNegativeObjectPropertyAssertion {
600            entity_iri: "http://example.org/a".into(),
601            property_iri: "http://example.org/p".into(),
602            target_iri: "http://example.org/b".into(),
603        })
604        .expect("invert nopa");
605        assert!(matches!(inverted, PatchOp::RemoveNegativeObjectPropertyAssertion { .. }));
606        let inverted = invert_patch_op(&PatchOp::AddSameIndividual {
607            individuals: vec!["http://example.org/a".into(), "http://example.org/b".into()],
608        })
609        .expect("invert same");
610        assert_eq!(
611            inverted,
612            PatchOp::RemoveSameIndividual {
613                individuals: vec!["http://example.org/a".into(), "http://example.org/b".into()],
614            }
615        );
616    }
617}