Skip to main content

strixonomy_swrl/
validate.rs

1//! SWRL rule validation (variables, DLSafe heuristics, builtins).
2
3use crate::builtins::is_supported_builtin;
4use crate::model::{SwrlAtom, SwrlDArg, SwrlDiagnostic, SwrlIArg, SwrlRule, SwrlSeverity};
5use std::collections::BTreeSet;
6
7const MAX_ATOMS: usize = 256;
8const MAX_IRI_LEN: usize = 2048;
9
10pub fn validate_rule(rule: &SwrlRule) -> Vec<SwrlDiagnostic> {
11    let mut diags = Vec::new();
12    if rule.body.is_empty() {
13        diags.push(SwrlDiagnostic {
14            code: "swrl_empty_body".into(),
15            severity: SwrlSeverity::Error,
16            message: "SWRL rule body must not be empty".into(),
17        });
18    }
19    if rule.head.is_empty() {
20        diags.push(SwrlDiagnostic {
21            code: "swrl_empty_head".into(),
22            severity: SwrlSeverity::Error,
23            message: "SWRL rule head must not be empty".into(),
24        });
25    }
26    let total_atoms = rule.body.len() + rule.head.len();
27    if total_atoms > MAX_ATOMS {
28        diags.push(SwrlDiagnostic {
29            code: "swrl_rule_too_large".into(),
30            severity: SwrlSeverity::Error,
31            message: format!("SWRL rule has {total_atoms} atoms; max is {MAX_ATOMS}"),
32        });
33    }
34
35    let mut body_vars = BTreeSet::new();
36    for atom in &rule.body {
37        collect_vars(atom, &mut body_vars);
38        check_atom(atom, &mut diags, false);
39    }
40    for atom in &rule.head {
41        check_atom(atom, &mut diags, true);
42        for v in atom_vars(atom) {
43            if !body_vars.contains(&v) {
44                diags.push(SwrlDiagnostic {
45                    code: "swrl_unbound_head_var".into(),
46                    severity: SwrlSeverity::Error,
47                    message: format!("variable ?{v} appears in head but is not bound in body"),
48                });
49            }
50        }
51    }
52
53    for atom in &rule.body {
54        warn_non_executable_atom(atom, false, &mut diags);
55    }
56    for atom in &rule.head {
57        warn_non_executable_atom(atom, true, &mut diags);
58        if matches!(atom, SwrlAtom::BuiltIn { .. }) {
59            diags.push(SwrlDiagnostic {
60                code: "swrl_builtin_in_head".into(),
61                severity: SwrlSeverity::Warning,
62                message: "Built-in atoms in rule heads are not executed by Strixonomy/Ontologos"
63                    .into(),
64            });
65        }
66    }
67
68    diags
69}
70
71fn warn_non_executable_atom(atom: &SwrlAtom, in_head: bool, diags: &mut Vec<SwrlDiagnostic>) {
72    let where_ = if in_head { "head" } else { "body" };
73    match atom {
74        SwrlAtom::BuiltIn { predicate, .. } => {
75            diags.push(SwrlDiagnostic {
76                code: "swrl_builtin_not_executable".into(),
77                severity: SwrlSeverity::Warning,
78                message: format!(
79                    "BuiltIn atom ({predicate}) in rule {where_} is not injected into Ontologos materialization; the rule will be skipped at classify"
80                ),
81            });
82        }
83        SwrlAtom::DataRange { range, .. } => {
84            diags.push(SwrlDiagnostic {
85                code: "swrl_datarange_not_executable".into(),
86                severity: SwrlSeverity::Warning,
87                message: format!(
88                    "DataRange atom ({range}) in rule {where_} is not injected into Ontologos materialization; the rule will be skipped at classify"
89                ),
90            });
91        }
92        _ => {}
93    }
94}
95
96fn check_atom(atom: &SwrlAtom, diags: &mut Vec<SwrlDiagnostic>, _in_head: bool) {
97    for iri in atom_iris(atom) {
98        if iri.trim().is_empty() {
99            diags.push(SwrlDiagnostic {
100                code: "swrl_empty_iri".into(),
101                severity: SwrlSeverity::Error,
102                message: "SWRL atom contains an empty IRI".into(),
103            });
104        } else if iri.len() > MAX_IRI_LEN {
105            diags.push(SwrlDiagnostic {
106                code: "swrl_iri_too_long".into(),
107                severity: SwrlSeverity::Error,
108                message: format!("SWRL IRI exceeds {MAX_IRI_LEN} characters"),
109            });
110        }
111    }
112    if let SwrlAtom::BuiltIn { predicate, .. } = atom {
113        if !is_supported_builtin(predicate) {
114            diags.push(SwrlDiagnostic {
115                code: "swrl_unsupported_builtin".into(),
116                severity: SwrlSeverity::Warning,
117                message: format!(
118                    "built-in {predicate} is not in the Strixonomy supported swrlb: registry"
119                ),
120            });
121        }
122    }
123}
124
125fn atom_iris(atom: &SwrlAtom) -> Vec<&str> {
126    match atom {
127        SwrlAtom::Class { class, arg } => {
128            let mut v = vec![class.as_str()];
129            if let SwrlIArg::Individual(i) = arg {
130                v.push(i.as_str());
131            }
132            v
133        }
134        SwrlAtom::ObjectProperty { property, subject, object } => {
135            let mut v = vec![property.as_str()];
136            if let SwrlIArg::Individual(i) = subject {
137                v.push(i.as_str());
138            }
139            if let SwrlIArg::Individual(i) = object {
140                v.push(i.as_str());
141            }
142            v
143        }
144        SwrlAtom::DataProperty { property, subject, value } => {
145            let mut v = vec![property.as_str()];
146            if let SwrlIArg::Individual(i) = subject {
147                v.push(i.as_str());
148            }
149            if let SwrlDArg::Literal { datatype: Some(dt), .. } = value {
150                v.push(dt.as_str());
151            }
152            v
153        }
154        SwrlAtom::BuiltIn { predicate, .. } => vec![predicate.as_str()],
155        SwrlAtom::DataRange { range, .. } => vec![range.as_str()],
156        SwrlAtom::SameIndividual { left, right }
157        | SwrlAtom::DifferentIndividuals { left, right } => {
158            let mut v = Vec::new();
159            if let SwrlIArg::Individual(i) = left {
160                v.push(i.as_str());
161            }
162            if let SwrlIArg::Individual(i) = right {
163                v.push(i.as_str());
164            }
165            v
166        }
167    }
168}
169
170fn collect_vars(atom: &SwrlAtom, vars: &mut BTreeSet<String>) {
171    for v in atom_vars(atom) {
172        vars.insert(v);
173    }
174}
175
176fn atom_vars(atom: &SwrlAtom) -> Vec<String> {
177    let mut out = Vec::new();
178    match atom {
179        SwrlAtom::Class { arg, .. } => push_i(arg, &mut out),
180        SwrlAtom::ObjectProperty { subject, object, .. } => {
181            push_i(subject, &mut out);
182            push_i(object, &mut out);
183        }
184        SwrlAtom::DataProperty { subject, value, .. } => {
185            push_i(subject, &mut out);
186            push_d(value, &mut out);
187        }
188        SwrlAtom::SameIndividual { left: a, right: b }
189        | SwrlAtom::DifferentIndividuals { left: a, right: b } => {
190            push_i(a, &mut out);
191            push_i(b, &mut out);
192        }
193        SwrlAtom::BuiltIn { args, .. } => {
194            for a in args {
195                push_d(a, &mut out);
196            }
197        }
198        SwrlAtom::DataRange { arg, .. } => push_d(arg, &mut out),
199    }
200    out
201}
202
203fn push_i(arg: &SwrlIArg, out: &mut Vec<String>) {
204    if let SwrlIArg::Variable(v) = arg {
205        out.push(v.clone());
206    }
207}
208
209fn push_d(arg: &SwrlDArg, out: &mut Vec<String>) {
210    if let SwrlDArg::Variable(v) = arg {
211        out.push(v.clone());
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::model::{SwrlAtom, SwrlIArg, SwrlRule};
219
220    #[test]
221    fn rejects_unbound_head_var() {
222        let rule = SwrlRule {
223            id: None,
224            body: vec![SwrlAtom::Class {
225                class: "http://ex#A".into(),
226                arg: SwrlIArg::Variable("x".into()),
227            }],
228            head: vec![SwrlAtom::Class {
229                class: "http://ex#B".into(),
230                arg: SwrlIArg::Variable("y".into()),
231            }],
232            enabled: true,
233        };
234        let d = validate_rule(&rule);
235        assert!(d.iter().any(|x| x.code == "swrl_unbound_head_var"));
236    }
237
238    #[test]
239    fn rejects_empty_iris() {
240        let rule = SwrlRule {
241            id: None,
242            body: vec![SwrlAtom::Class { class: "".into(), arg: SwrlIArg::Variable("x".into()) }],
243            head: vec![SwrlAtom::Class {
244                class: "http://ex#B".into(),
245                arg: SwrlIArg::Variable("x".into()),
246            }],
247            enabled: true,
248        };
249        let d = validate_rule(&rule);
250        assert!(d.iter().any(|x| x.code == "swrl_empty_iri"));
251    }
252
253    #[test]
254    fn warns_builtin_and_datarange_not_executable() {
255        let rule = SwrlRule {
256            id: None,
257            body: vec![
258                SwrlAtom::Class {
259                    class: "http://ex#A".into(),
260                    arg: SwrlIArg::Variable("x".into()),
261                },
262                SwrlAtom::BuiltIn {
263                    predicate: "http://www.w3.org/2003/11/swrlb#equal".into(),
264                    args: vec![],
265                },
266                SwrlAtom::DataRange {
267                    range: "http://www.w3.org/2001/XMLSchema#string".into(),
268                    arg: crate::model::SwrlDArg::Variable("x".into()),
269                },
270            ],
271            head: vec![SwrlAtom::Class {
272                class: "http://ex#B".into(),
273                arg: SwrlIArg::Variable("x".into()),
274            }],
275            enabled: true,
276        };
277        let d = validate_rule(&rule);
278        assert!(d.iter().any(|x| x.code == "swrl_builtin_not_executable"));
279        assert!(d.iter().any(|x| x.code == "swrl_datarange_not_executable"));
280    }
281
282    #[test]
283    fn foreign_namespace_builtin_is_unsupported() {
284        let rule = SwrlRule {
285            id: None,
286            body: vec![SwrlAtom::BuiltIn {
287                predicate: "http://evil.example/equal".into(),
288                args: vec![],
289            }],
290            head: vec![SwrlAtom::Class {
291                class: "http://ex#B".into(),
292                arg: SwrlIArg::Variable("x".into()),
293            }],
294            enabled: true,
295        };
296        let d = validate_rule(&rule);
297        assert!(d.iter().any(|x| x.code == "swrl_unsupported_builtin"));
298        assert!(d.iter().any(|x| x.code == "swrl_builtin_not_executable"));
299    }
300}