Skip to main content

pyshacl_turbo_engine/
ontology.rs

1//! pySHACL ontology inoculation: schema axioms and explicit NamedIndividuals.
2
3use std::collections::{HashMap, HashSet, VecDeque};
4
5use oxigraph::model::*;
6
7use crate::graph::{RDF, RDFS, iri, with_union};
8
9#[cfg(test)]
10#[path = "ontology_tests.rs"]
11mod tests;
12
13const OWL: &str = "http://www.w3.org/2002/07/owl#";
14const RDFS_CLASSES: &[&str] = &[
15    "Class",
16    "Container",
17    "ContainerMembershipProperty",
18    "Datatype",
19    "Literal",
20    "Resource",
21];
22const RDFS_PROPERTIES: &[&str] = &[
23    "comment",
24    "domain",
25    "isDefinedBy",
26    "label",
27    "member",
28    "range",
29    "seeAlso",
30    "subClassOf",
31    "subPropertyOf",
32];
33const OWL_CLASSES: &[&str] = &[
34    "AllDifferent",
35    "AllDisjointClasses",
36    "AllDisjointProperties",
37    "Annotation",
38    "AnnotationProperty",
39    "AsymmetricProperty",
40    "Axiom",
41    "Class",
42    "DataRange",
43    "DatatypeProperty",
44    "DeprecatedClass",
45    "DeprecatedProperty",
46    "FunctionalProperty",
47    "InverseFunctionalProperty",
48    "IrreflexiveProperty",
49    "NamedIndividual",
50    "NegativePropertyAssertion",
51    "ObjectProperty",
52    "Ontology",
53    "OntologyProperty",
54    "ReflexiveProperty",
55    "Restriction",
56    "SymmetricProperty",
57    "TransitiveProperty",
58];
59const OWL_PROPERTIES: &[&str] = &[
60    "allValuesFrom",
61    "annotatedProperty",
62    "annotatedSource",
63    "annotatedTarget",
64    "assertionProperty",
65    "cardinality",
66    "complementOf",
67    "datatypeComplementOf",
68    "differentFrom",
69    "disjointUnionOf",
70    "disjointWith",
71    "distinctMembers",
72    "equivalentClass",
73    "equivalentProperty",
74    "hasKey",
75    "hasSelf",
76    "hasValue",
77    "intersectionOf",
78    "inverseOf",
79    "maxCardinality",
80    "maxQualifiedCardinality",
81    "members",
82    "minCardinality",
83    "minQualifiedCardinality",
84    "onClass",
85    "onDataRange",
86    "onDatatype",
87    "onProperties",
88    "onProperty",
89    "oneOf",
90    "propertyChainAxiom",
91    "propertyDisjointWith",
92    "qualifiedCardinality",
93    "sameAs",
94    "someValuesFrom",
95    "sourceIndividual",
96    "targetIndividual",
97    "targetValue",
98    "unionOf",
99    "withRestrictions",
100    "backwardCompatibleWith",
101    "deprecated",
102    "incompatibleWith",
103    "priorVersion",
104    "versionInfo",
105    "bottomDataProperty",
106    "topDataProperty",
107    "bottomObjectProperty",
108    "topObjectProperty",
109    "imports",
110    "versionIRI",
111];
112
113fn recognized(node: NamedNodeRef<'_>, rdfs: &[&str], owl: &[&str]) -> bool {
114    node.as_str()
115        .strip_prefix(RDFS)
116        .is_some_and(|s| rdfs.contains(&s))
117        || node
118            .as_str()
119            .strip_prefix(OWL)
120            .is_some_and(|s| owl.contains(&s))
121}
122
123#[derive(Default)]
124struct Copies {
125    blanks: HashMap<BlankNode, BlankNode>,
126    pending: VecDeque<(NamedOrBlankNode, bool)>,
127}
128
129impl Copies {
130    fn subject(&mut self, node: NamedOrBlankNodeRef<'_>) -> NamedOrBlankNode {
131        match node {
132            NamedOrBlankNodeRef::NamedNode(n) => n.into_owned().into(),
133            NamedOrBlankNodeRef::BlankNode(n) => {
134                self.pending.push_back((n.into_owned().into(), false));
135                self.blanks
136                    .entry(n.into_owned())
137                    .or_default()
138                    .clone()
139                    .into()
140            }
141        }
142    }
143
144    fn object(&mut self, node: TermRef<'_>, deep: bool) -> Term {
145        match node {
146            TermRef::BlankNode(n) => Term::from(self.subject(n.into())),
147            TermRef::NamedNode(n) if deep => {
148                self.pending.push_back((n.into_owned().into(), true));
149                n.into_owned().into()
150            }
151            _ => node.into_owned(),
152        }
153    }
154}
155
156pub fn inoculate(data: &mut Dataset, ontology: &Dataset) {
157    let ontology = with_union(ontology);
158    let mut copies = Copies::default();
159    let individuals: HashSet<_> = ontology
160        .quads_for_pattern(
161            None,
162            Some(iri(RDF, "type").as_ref()),
163            Some(iri(OWL, "NamedIndividual").as_ref().into()),
164            Some(GraphNameRef::DefaultGraph),
165        )
166        .map(|q| q.subject.into_owned())
167        .collect();
168    for quad in ontology.iter().filter(|q| q.graph_name.is_default_graph()) {
169        let schema_type = quad.predicate == iri(RDF, "type").as_ref()
170            && matches!(quad.object, TermRef::NamedNode(n) if recognized(n, RDFS_CLASSES, OWL_CLASSES));
171        let incoming = match quad.object {
172            TermRef::NamedNode(n) => individuals.contains(&n.into_owned().into()),
173            TermRef::BlankNode(n) => individuals.contains(&n.into_owned().into()),
174            _ => false,
175        };
176        if schema_type || recognized(quad.predicate, RDFS_PROPERTIES, OWL_PROPERTIES) || incoming {
177            let subject = copies.subject(quad.subject);
178            let object = copies.object(quad.object, false);
179            data.insert(&Quad::new(
180                subject,
181                quad.predicate,
182                object,
183                GraphName::DefaultGraph,
184            ));
185        }
186    }
187    copies
188        .pending
189        .extend(individuals.into_iter().map(|n| (n, true)));
190    let mut visited = HashSet::new();
191    while let Some((node, deep)) = copies.pending.pop_front() {
192        if !visited.insert((node.clone(), deep)) {
193            continue;
194        }
195        let subject = copies.subject(node.as_ref());
196        for quad in ontology.quads_for_pattern(
197            Some(node.as_ref()),
198            None,
199            None,
200            Some(GraphNameRef::DefaultGraph),
201        ) {
202            let object = copies.object(quad.object, deep);
203            data.insert(&Quad::new(
204                subject.clone(),
205                quad.predicate,
206                object,
207                GraphName::DefaultGraph,
208            ));
209        }
210    }
211}