Skip to main content

strixonomy_swrl/
model.rs

1//! Canonical Strixonomy SWRL IR (aligns with horned-owl Atom; keeps BuiltIns).
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum SwrlIArg {
8    Individual(String),
9    Variable(String),
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum SwrlDArg {
15    Literal { lexical: String, datatype: Option<String> },
16    Variable(String),
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(tag = "kind", rename_all = "snake_case")]
21pub enum SwrlAtom {
22    Class { class: String, arg: SwrlIArg },
23    ObjectProperty { property: String, subject: SwrlIArg, object: SwrlIArg },
24    DataProperty { property: String, subject: SwrlIArg, value: SwrlDArg },
25    SameIndividual { left: SwrlIArg, right: SwrlIArg },
26    DifferentIndividuals { left: SwrlIArg, right: SwrlIArg },
27    BuiltIn { predicate: String, args: Vec<SwrlDArg> },
28    DataRange { range: String, arg: SwrlDArg },
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct SwrlRule {
33    /// Stable id when known (blank node or named rule IRI).
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub id: Option<String>,
36    pub body: Vec<SwrlAtom>,
37    pub head: Vec<SwrlAtom>,
38    #[serde(default = "default_enabled")]
39    pub enabled: bool,
40}
41
42fn default_enabled() -> bool {
43    true
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct SwrlRuleSummary {
48    pub id: String,
49    pub label: String,
50    pub body_count: usize,
51    pub head_count: usize,
52    pub enabled: bool,
53}
54
55impl SwrlRule {
56    pub fn summary(&self, index: usize) -> SwrlRuleSummary {
57        let id = self.id.clone().unwrap_or_else(|| format!("rule-{index}"));
58        let label = format!(
59            "{} → {}",
60            self.body.len(),
61            self.head.first().map(atom_short).unwrap_or_else(|| "∅".into())
62        );
63        SwrlRuleSummary {
64            id,
65            label,
66            body_count: self.body.len(),
67            head_count: self.head.len(),
68            enabled: self.enabled,
69        }
70    }
71
72    pub fn referenced_iris(&self) -> Vec<String> {
73        let mut out = Vec::new();
74        for atom in self.body.iter().chain(self.head.iter()) {
75            collect_iris(atom, &mut out);
76        }
77        out.sort();
78        out.dedup();
79        out
80    }
81
82    /// Rewrite absolute IRIs in the rule (`from` → `to`). Returns `true` if anything changed.
83    pub fn remap_iri(&mut self, from: &str, to: &str) -> bool {
84        if from == to {
85            return false;
86        }
87        let mut changed = false;
88        for atom in self.body.iter_mut().chain(self.head.iter_mut()) {
89            if remap_atom(atom, from, to) {
90                changed = true;
91            }
92        }
93        if let Some(id) = &mut self.id {
94            if id == from {
95                *id = to.to_string();
96                changed = true;
97            }
98        }
99        changed
100    }
101}
102
103fn remap_atom(atom: &mut SwrlAtom, from: &str, to: &str) -> bool {
104    let mut changed = false;
105    let replace = |s: &mut String| {
106        if s.as_str() == from {
107            *s = to.to_string();
108            true
109        } else {
110            false
111        }
112    };
113    match atom {
114        SwrlAtom::Class { class, arg } => {
115            changed |= replace(class);
116            if let SwrlIArg::Individual(i) = arg {
117                changed |= replace(i);
118            }
119        }
120        SwrlAtom::ObjectProperty { property, subject, object } => {
121            changed |= replace(property);
122            if let SwrlIArg::Individual(i) = subject {
123                changed |= replace(i);
124            }
125            if let SwrlIArg::Individual(i) = object {
126                changed |= replace(i);
127            }
128        }
129        SwrlAtom::DataProperty { property, subject, .. } => {
130            changed |= replace(property);
131            if let SwrlIArg::Individual(i) = subject {
132                changed |= replace(i);
133            }
134        }
135        SwrlAtom::SameIndividual { left: a, right: b }
136        | SwrlAtom::DifferentIndividuals { left: a, right: b } => {
137            if let SwrlIArg::Individual(i) = a {
138                changed |= replace(i);
139            }
140            if let SwrlIArg::Individual(i) = b {
141                changed |= replace(i);
142            }
143        }
144        SwrlAtom::BuiltIn { predicate, .. } => {
145            changed |= replace(predicate);
146        }
147        SwrlAtom::DataRange { range, .. } => {
148            changed |= replace(range);
149        }
150    }
151    changed
152}
153
154fn atom_short(atom: &SwrlAtom) -> String {
155    match atom {
156        SwrlAtom::Class { class, .. } => short(class),
157        SwrlAtom::ObjectProperty { property, .. } => short(property),
158        SwrlAtom::DataProperty { property, .. } => short(property),
159        SwrlAtom::BuiltIn { predicate, .. } => short(predicate),
160        SwrlAtom::SameIndividual { .. } => "sameAs".into(),
161        SwrlAtom::DifferentIndividuals { .. } => "differentFrom".into(),
162        SwrlAtom::DataRange { range, .. } => short(range),
163    }
164}
165
166fn short(iri: &str) -> String {
167    iri.rsplit(['#', '/']).next().unwrap_or(iri).to_string()
168}
169
170fn collect_iris(atom: &SwrlAtom, out: &mut Vec<String>) {
171    match atom {
172        SwrlAtom::Class { class, arg } => {
173            out.push(class.clone());
174            if let SwrlIArg::Individual(i) = arg {
175                out.push(i.clone());
176            }
177        }
178        SwrlAtom::ObjectProperty { property, subject, object } => {
179            out.push(property.clone());
180            if let SwrlIArg::Individual(i) = subject {
181                out.push(i.clone());
182            }
183            if let SwrlIArg::Individual(i) = object {
184                out.push(i.clone());
185            }
186        }
187        SwrlAtom::DataProperty { property, subject, .. } => {
188            out.push(property.clone());
189            if let SwrlIArg::Individual(i) = subject {
190                out.push(i.clone());
191            }
192        }
193        SwrlAtom::SameIndividual { left: a, right: b }
194        | SwrlAtom::DifferentIndividuals { left: a, right: b } => {
195            if let SwrlIArg::Individual(i) = a {
196                out.push(i.clone());
197            }
198            if let SwrlIArg::Individual(i) = b {
199                out.push(i.clone());
200            }
201        }
202        SwrlAtom::BuiltIn { predicate, .. } => out.push(predicate.clone()),
203        SwrlAtom::DataRange { range, .. } => out.push(range.clone()),
204    }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub enum SwrlSeverity {
210    Error,
211    Warning,
212    Info,
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216pub struct SwrlDiagnostic {
217    pub code: String,
218    pub severity: SwrlSeverity,
219    pub message: String,
220}