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" => ruleset.subproperty_transitivity = true,
56 "domain_range" | "dr" => ruleset.domain_range = true,
57 "inverse" | "inverse_of" => ruleset.inverse_of = true,
58 "symmetric" | "symmetric_property" => ruleset.symmetric_property = true,
59 "transitive" | "transitive_property" => ruleset.transitive_property = true,
60 "rdfs" => ruleset = Self::rdfs(),
61 "owlrl" | "owl-rl" => ruleset = Self::owlrl(),
62 _ => {}
63 }
64 }
65 ruleset
66 }
67}
68
69pub struct SynapseReasoner {
71 strategy: ReasoningStrategy,
72 rules: RuleSet,
73}
74
75impl SynapseReasoner {
76 pub fn new(strategy: ReasoningStrategy) -> Self {
77 let rules = match strategy {
78 ReasoningStrategy::RDFS => RuleSet::rdfs(),
79 ReasoningStrategy::OWLRL => RuleSet::owlrl(),
80 ReasoningStrategy::None => RuleSet::default(),
81 };
82 Self { strategy, rules }
83 }
84
85 pub fn with_rules(strategy: ReasoningStrategy, rules: RuleSet) -> Self {
86 Self { strategy, rules }
87 }
88
89 pub fn rules(&self) -> &RuleSet {
91 &self.rules
92 }
93
94 pub fn apply(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
96 match self.strategy {
97 ReasoningStrategy::None => Ok(Vec::new()),
98 ReasoningStrategy::RDFS => self.apply_rdfs_reasoning(store),
99 ReasoningStrategy::OWLRL => self.apply_owl_reasoning(store),
100 }
101 }
102
103 fn apply_rdfs_reasoning(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
104 let mut inferred = Vec::new();
105 let sub_class_of = NamedNode::new("http://www.w3.org/2000/01/rdf-schema#subClassOf")?;
106
107 for quad in store.iter() {
108 if let Ok(q) = quad {
109 if q.predicate == sub_class_of {
110 let subject_b = q.subject.clone();
113 if let Subject::NamedNode(subj_node) = subject_b {
114 for inner_quad in store.iter() {
115 if let Ok(iq) = inner_quad {
116 if iq.predicate == sub_class_of && iq.object == subj_node.clone().into() {
117 inferred.push((
119 iq.subject.to_string(),
120 sub_class_of.to_string(),
121 q.object.to_string(),
122 ));
123 }
124 }
125 }
126 }
127 }
128 }
129 }
130
131 Ok(inferred)
132 }
133
134 fn apply_owl_reasoning(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
135 let mut inferred = Vec::new();
136 let rules = &self.rules;
137
138 if rules.symmetric_property {
140 let type_prop = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?;
141 let symmetric_class = NamedNode::new("http://www.w3.org/2002/07/owl#SymmetricProperty")?;
142
143 for quad in store.quads_for_pattern(None, Some(type_prop.as_ref().into()), Some(symmetric_class.as_ref().into()), None) {
144 if let Ok(q) = quad {
145 if let Subject::NamedNode(p_node) = q.subject {
147 let p_ref = p_node.as_ref();
148
149 for edge in store.quads_for_pattern(None, Some(p_ref.into()), None, None) {
151 if let Ok(e) = edge {
152 if let Term::NamedNode(obj_node) = e.object {
154 let s_str = e.subject.to_string();
155 let p_str = p_node.to_string();
156 let o_str = obj_node.to_string();
157 inferred.push((o_str, p_str, s_str));
158 }
159 }
160 }
161 }
162 }
163 }
164 }
165
166 if rules.transitive_property {
168 let type_prop = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?;
169 let transitive_class = NamedNode::new("http://www.w3.org/2002/07/owl#TransitiveProperty")?;
170
171 for quad in store.quads_for_pattern(None, Some(type_prop.as_ref().into()), Some(transitive_class.as_ref().into()), None) {
172 if let Ok(q) = quad {
173 if let Subject::NamedNode(p_node) = q.subject {
174 let p_ref = p_node.as_ref();
175
176 for xy in store.quads_for_pattern(None, Some(p_ref.into()), None, None) {
178 if let Ok(xy_quad) = xy {
179 if let Term::NamedNode(y) = xy_quad.object {
180 for yz in store.quads_for_pattern(Some(y.as_ref().into()), Some(p_ref.into()), None, None) {
182 if let Ok(yz_quad) = yz {
183 inferred.push((
184 xy_quad.subject.to_string(),
185 p_node.to_string(),
186 yz_quad.object.to_string()
187 ));
188 }
189 }
190 }
191 }
192 }
193 }
194 }
195 }
196 }
197
198 if rules.inverse_of {
200 let inverse_prop = NamedNode::new("http://www.w3.org/2002/07/owl#inverseOf")?;
201
202 for quad in store.quads_for_pattern(None, Some(inverse_prop.as_ref().into()), None, None) {
203 if let Ok(q) = quad {
204 if let Subject::NamedNode(p1_node) = q.subject {
205 let p1_ref = p1_node.as_ref();
206 if let Term::NamedNode(p2_node) = q.object {
207 for edge in store.quads_for_pattern(None, Some(p1_ref.into()), None, None) {
209 if let Ok(e) = edge {
210 if let Term::NamedNode(y) = e.object {
211 inferred.push((
212 y.to_string(),
213 p2_node.to_string(),
214 e.subject.to_string()
215 ));
216 }
217 }
218 }
219 }
220 }
221 }
222 }
223 }
224
225 Ok(inferred)
226 }
227 pub fn materialize(&self, store: &Store) -> Result<usize> {
228 let inferred = self.apply(store)?;
229 let mut count = 0;
230 let mut skipped = 0;
231
232 for (s, p, o) in inferred {
233 if let (Ok(subject), Ok(predicate), Ok(object)) = (
234 NamedNode::new(&s),
235 NamedNode::new(&p),
236 NamedNode::new(&o),
237 ) {
238 let quad = Quad::new(subject.clone(), predicate.clone(), object.clone(), GraphName::DefaultGraph);
240 if store.contains(&quad)? {
241 skipped += 1;
242 continue;
243 }
244 let _ = store.insert(&quad);
246 count += 1;
247 }
248 }
249
250 if skipped > 0 {
251 eprintln!("Reasoning: {} new triples, {} duplicates skipped", count, skipped);
252 }
253
254 Ok(count)
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use oxigraph::model::NamedNode;
262
263 #[test]
264 fn test_rdfs_transitivity() -> Result<()> {
265 let store = Store::new()?;
266 let sub_class_of = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
267
268 let a = NamedNode::new("http://example.org/A")?;
269 let b = NamedNode::new("http://example.org/B")?;
270 let c = NamedNode::new("http://example.org/C")?;
271 let pred = NamedNode::new(sub_class_of)?;
272
273 store.insert(&Quad::new(a.clone(), pred.clone(), b.clone(), GraphName::DefaultGraph))?;
275 store.insert(&Quad::new(b.clone(), pred.clone(), c.clone(), GraphName::DefaultGraph))?;
276
277 let reasoner = SynapseReasoner::new(ReasoningStrategy::RDFS);
278 let inferred = reasoner.apply(&store)?;
279
280 let mut found = false;
282 let expected_s = a.to_string();
283 let expected_o = c.to_string();
284
285 for (s, _p, o) in inferred {
286 if s == expected_s && o == expected_o {
287 found = true;
288 break;
289 }
290 }
291
292 assert!(found, "Inferred A subClassOf C not found");
293 Ok(())
294 }
295
296 #[test]
297 fn test_owl_reasoning_smoke() -> Result<()> {
298 let store = Store::new()?;
299 let reasoner = SynapseReasoner::new(ReasoningStrategy::OWLRL);
300
301 let inferred = reasoner.apply(&store)?;
302 println!("OWL Reasoner inferred {} default triples", inferred.len());
305 Ok(())
306 }
307}