1use anyhow::Result;
2use oxigraph::model::*;
3use oxigraph::store::Store;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ReasoningStrategy {
8 None,
9 RDFS,
10 OWLRL,
11}
12
13#[derive(Debug, Clone, Default)]
15pub struct RuleSet {
16 pub subclass_transitivity: bool,
17 pub subproperty_transitivity: bool,
18 pub domain_range: bool,
19 pub inverse_of: bool,
20 pub symmetric_property: bool,
21 pub transitive_property: bool,
22}
23
24impl RuleSet {
25 pub fn rdfs() -> Self {
27 Self {
28 subclass_transitivity: true,
29 subproperty_transitivity: true,
30 domain_range: true,
31 inverse_of: false,
32 symmetric_property: false,
33 transitive_property: false,
34 }
35 }
36
37 pub fn owlrl() -> Self {
39 Self {
40 subclass_transitivity: true,
41 subproperty_transitivity: true,
42 domain_range: true,
43 inverse_of: true,
44 symmetric_property: true,
45 transitive_property: true,
46 }
47 }
48
49 pub fn from_str(rules: &str) -> Self {
51 let mut ruleset = Self::default();
52 for rule in rules.split(',').map(|s| s.trim().to_lowercase()) {
53 match rule.as_str() {
54 "subclass" | "subclass_transitivity" => ruleset.subclass_transitivity = true,
55 "subproperty" | "subproperty_transitivity" => {
56 ruleset.subproperty_transitivity = true
57 }
58 "domain_range" | "dr" => ruleset.domain_range = true,
59 "inverse" | "inverse_of" => ruleset.inverse_of = true,
60 "symmetric" | "symmetric_property" => ruleset.symmetric_property = true,
61 "transitive" | "transitive_property" => ruleset.transitive_property = true,
62 "rdfs" => ruleset = Self::rdfs(),
63 "owlrl" | "owl-rl" => ruleset = Self::owlrl(),
64 _ => {}
65 }
66 }
67 ruleset
68 }
69}
70
71pub struct SynapseReasoner {
73 strategy: ReasoningStrategy,
74 rules: RuleSet,
75}
76
77impl SynapseReasoner {
78 pub fn new(strategy: ReasoningStrategy) -> Self {
79 let rules = match strategy {
80 ReasoningStrategy::RDFS => RuleSet::rdfs(),
81 ReasoningStrategy::OWLRL => RuleSet::owlrl(),
82 ReasoningStrategy::None => RuleSet::default(),
83 };
84 Self { strategy, rules }
85 }
86
87 pub fn with_rules(strategy: ReasoningStrategy, rules: RuleSet) -> Self {
88 Self { strategy, rules }
89 }
90
91 pub fn rules(&self) -> &RuleSet {
93 &self.rules
94 }
95
96 pub fn apply(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
98 match self.strategy {
99 ReasoningStrategy::None => Ok(Vec::new()),
100 ReasoningStrategy::RDFS => self.apply_rdfs_reasoning(store),
101 ReasoningStrategy::OWLRL => self.apply_owl_reasoning(store),
102 }
103 }
104
105 fn apply_rdfs_reasoning(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
106 let mut inferred = Vec::new();
107 let sub_class_of = NamedNode::new("http://www.w3.org/2000/01/rdf-schema#subClassOf")?;
108
109 for quad in store.iter() {
110 if let Ok(q) = quad {
111 if q.predicate == sub_class_of {
112 let subject_b = q.subject.clone();
115 if let Subject::NamedNode(subj_node) = subject_b {
116 for inner_quad in store.iter() {
117 if let Ok(iq) = inner_quad {
118 if iq.predicate == sub_class_of
119 && iq.object == subj_node.clone().into()
120 {
121 inferred.push((
123 iq.subject.to_string(),
124 sub_class_of.to_string(),
125 q.object.to_string(),
126 ));
127 }
128 }
129 }
130 }
131 }
132 }
133 }
134
135 Ok(inferred)
136 }
137
138 fn apply_owl_reasoning(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
139 let mut inferred = Vec::new();
140 let rules = &self.rules;
141
142 if rules.symmetric_property {
144 let type_prop = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?;
145 let symmetric_class =
146 NamedNode::new("http://www.w3.org/2002/07/owl#SymmetricProperty")?;
147
148 for quad in store.quads_for_pattern(
149 None,
150 Some(type_prop.as_ref()),
151 Some(symmetric_class.as_ref().into()),
152 None,
153 ) {
154 if let Ok(q) = quad {
155 if let Subject::NamedNode(p_node) = q.subject {
157 let p_ref = p_node.as_ref();
158
159 for edge in store.quads_for_pattern(None, Some(p_ref), None, None) {
161 if let Ok(e) = edge {
162 if let Term::NamedNode(obj_node) = e.object {
164 let s_str = e.subject.to_string();
165 let p_str = p_node.to_string();
166 let o_str = obj_node.to_string();
167 inferred.push((o_str, p_str, s_str));
168 }
169 }
170 }
171 }
172 }
173 }
174 }
175
176 if rules.transitive_property {
178 let type_prop = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?;
179 let transitive_class =
180 NamedNode::new("http://www.w3.org/2002/07/owl#TransitiveProperty")?;
181
182 for quad in store.quads_for_pattern(
183 None,
184 Some(type_prop.as_ref()),
185 Some(transitive_class.as_ref().into()),
186 None,
187 ) {
188 if let Ok(q) = quad {
189 if let Subject::NamedNode(p_node) = q.subject {
190 let p_ref = p_node.as_ref();
191
192 for xy in store.quads_for_pattern(None, Some(p_ref), None, None) {
194 if let Ok(xy_quad) = xy {
195 if let Term::NamedNode(y) = xy_quad.object {
196 for yz_quad in store
198 .quads_for_pattern(
199 Some(y.as_ref().into()),
200 Some(p_ref),
201 None,
202 None,
203 )
204 .flatten()
205 {
206 inferred.push((
207 xy_quad.subject.to_string(),
208 p_node.to_string(),
209 yz_quad.object.to_string(),
210 ));
211 }
212 }
213 }
214 }
215 }
216 }
217 }
218 }
219
220 if rules.inverse_of {
222 let inverse_prop = NamedNode::new("http://www.w3.org/2002/07/owl#inverseOf")?;
223
224 for quad in store.quads_for_pattern(None, Some(inverse_prop.as_ref()), None, None) {
225 if let Ok(q) = quad {
226 if let Subject::NamedNode(p1_node) = q.subject {
227 let p1_ref = p1_node.as_ref();
228 if let Term::NamedNode(p2_node) = q.object {
229 for e in store
231 .quads_for_pattern(None, Some(p1_ref), None, None)
232 .flatten()
233 {
234 if let Term::NamedNode(y) = e.object {
235 inferred.push((
236 y.to_string(),
237 p2_node.to_string(),
238 e.subject.to_string(),
239 ));
240 }
241 }
242 }
243 }
244 }
245 }
246 }
247
248 Ok(inferred)
249 }
250 pub fn materialize(&self, store: &Store) -> Result<usize> {
251 let mut total_added = 0;
252
253 loop {
255 let inferred = self.apply(store)?;
256 let mut added_in_pass = 0;
257 let mut skipped = 0;
258
259 for (s, p, o) in inferred {
260 if let (Ok(subject), Ok(predicate), Ok(object)) =
261 (NamedNode::new(&s), NamedNode::new(&p), NamedNode::new(&o))
262 {
263 let quad = Quad::new(
265 subject.clone(),
266 predicate.clone(),
267 object.clone(),
268 GraphName::DefaultGraph,
269 );
270 if store.contains(&quad)? {
271 skipped += 1;
272 continue;
273 }
274 let _ = store.insert(&quad);
276 added_in_pass += 1;
277 }
278 }
279
280 if skipped > 0 {
281 eprintln!(
282 "Reasoning pass: {} new triples, {} duplicates skipped",
283 added_in_pass, skipped
284 );
285 }
286
287 if added_in_pass == 0 {
288 break;
289 }
290
291 total_added += added_in_pass;
292 }
293
294 Ok(total_added)
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use oxigraph::model::NamedNode;
302
303 #[test]
304 fn test_rdfs_transitivity() -> Result<()> {
305 let store = Store::new()?;
306 let sub_class_of = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
307
308 let a = NamedNode::new("http://example.org/A")?;
309 let b = NamedNode::new("http://example.org/B")?;
310 let c = NamedNode::new("http://example.org/C")?;
311 let pred = NamedNode::new(sub_class_of)?;
312
313 store.insert(&Quad::new(
315 a.clone(),
316 pred.clone(),
317 b.clone(),
318 GraphName::DefaultGraph,
319 ))?;
320 store.insert(&Quad::new(
321 b.clone(),
322 pred.clone(),
323 c.clone(),
324 GraphName::DefaultGraph,
325 ))?;
326
327 let reasoner = SynapseReasoner::new(ReasoningStrategy::RDFS);
328 let inferred = reasoner.apply(&store)?;
329
330 let mut found = false;
332 let expected_s = a.to_string();
333 let expected_o = c.to_string();
334
335 for (s, _p, o) in inferred {
336 if s == expected_s && o == expected_o {
337 found = true;
338 break;
339 }
340 }
341
342 assert!(found, "Inferred A subClassOf C not found");
343 Ok(())
344 }
345
346 #[test]
347 fn test_owl_reasoning_smoke() -> Result<()> {
348 let store = Store::new()?;
349 let reasoner = SynapseReasoner::new(ReasoningStrategy::OWLRL);
350
351 let inferred = reasoner.apply(&store)?;
352 println!("OWL Reasoner inferred {} default triples", inferred.len());
355 Ok(())
356 }
357}