1use 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}