tensorlogic_oxirs_bridge/
ontology_diff.rs1use serde::{Deserialize, Serialize};
8use tensorlogic_adapters::SymbolTable;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub enum DiffEntry {
15 Added(String),
17 Removed(String),
19 Modified { before: String, after: String },
21}
22
23impl DiffEntry {
24 pub fn name(&self) -> &str {
26 match self {
27 DiffEntry::Added(n) => n.as_str(),
28 DiffEntry::Removed(n) => n.as_str(),
29 DiffEntry::Modified { before, .. } => before.as_str(),
30 }
31 }
32
33 pub fn is_addition(&self) -> bool {
35 matches!(self, DiffEntry::Added(_))
36 }
37
38 pub fn is_removal(&self) -> bool {
40 matches!(self, DiffEntry::Removed(_))
41 }
42
43 pub fn is_modification(&self) -> bool {
45 matches!(self, DiffEntry::Modified { .. })
46 }
47}
48
49#[derive(Debug, Clone, Default, Serialize, Deserialize)]
53pub struct OntologyDiff {
54 pub domain_diffs: Vec<DiffEntry>,
56 pub predicate_diffs: Vec<DiffEntry>,
58}
59
60impl OntologyDiff {
61 pub fn new() -> Self {
63 OntologyDiff::default()
64 }
65
66 pub fn is_empty(&self) -> bool {
68 self.domain_diffs.is_empty() && self.predicate_diffs.is_empty()
69 }
70
71 pub fn total_changes(&self) -> usize {
73 self.domain_diffs.len() + self.predicate_diffs.len()
74 }
75
76 pub fn report(&self) -> String {
78 let mut out = String::new();
79 out.push_str("OntologyDiff Report\n");
80 out.push_str("===================\n");
81
82 if self.domain_diffs.is_empty() {
83 out.push_str("Domains: no changes\n");
84 } else {
85 out.push_str("Domains:\n");
86 for entry in &self.domain_diffs {
87 match entry {
88 DiffEntry::Added(n) => {
89 out.push_str(&format!(" + Added: {}\n", n));
90 }
91 DiffEntry::Removed(n) => {
92 out.push_str(&format!(" - Removed: {}\n", n));
93 }
94 DiffEntry::Modified { before, after } => {
95 out.push_str(&format!(" ~ Modified: {} -> {}\n", before, after));
96 }
97 }
98 }
99 }
100
101 if self.predicate_diffs.is_empty() {
102 out.push_str("Predicates: no changes\n");
103 } else {
104 out.push_str("Predicates:\n");
105 for entry in &self.predicate_diffs {
106 match entry {
107 DiffEntry::Added(n) => {
108 out.push_str(&format!(" + Added: {}\n", n));
109 }
110 DiffEntry::Removed(n) => {
111 out.push_str(&format!(" - Removed: {}\n", n));
112 }
113 DiffEntry::Modified { before, after } => {
114 out.push_str(&format!(" ~ Modified: {} -> {}\n", before, after));
115 }
116 }
117 }
118 }
119
120 out
121 }
122
123 pub fn summary(&self) -> String {
125 let added = self.domain_diffs.iter().filter(|e| e.is_addition()).count()
126 + self
127 .predicate_diffs
128 .iter()
129 .filter(|e| e.is_addition())
130 .count();
131 let removed = self.domain_diffs.iter().filter(|e| e.is_removal()).count()
132 + self
133 .predicate_diffs
134 .iter()
135 .filter(|e| e.is_removal())
136 .count();
137 let modified = self
138 .domain_diffs
139 .iter()
140 .filter(|e| e.is_modification())
141 .count()
142 + self
143 .predicate_diffs
144 .iter()
145 .filter(|e| e.is_modification())
146 .count();
147 format!(
148 "OntologyDiff: {} added, {} removed, {} modified ({} total)",
149 added,
150 removed,
151 modified,
152 self.total_changes()
153 )
154 }
155}
156
157pub fn compare_symbol_tables(a: &SymbolTable, b: &SymbolTable) -> OntologyDiff {
175 let mut diff = OntologyDiff::new();
176
177 for (name, b_info) in &b.domains {
179 match a.domains.get(name) {
180 None => {
181 diff.domain_diffs.push(DiffEntry::Added(name.clone()));
182 }
183 Some(a_info) => {
184 if a_info.cardinality != b_info.cardinality {
185 diff.domain_diffs.push(DiffEntry::Modified {
186 before: format!("{}(cardinality={})", name, a_info.cardinality),
187 after: format!("{}(cardinality={})", name, b_info.cardinality),
188 });
189 }
190 }
191 }
192 }
193 for name in a.domains.keys() {
194 if !b.domains.contains_key(name) {
195 diff.domain_diffs.push(DiffEntry::Removed(name.clone()));
196 }
197 }
198
199 for (name, b_pred) in &b.predicates {
201 match a.predicates.get(name) {
202 None => {
203 diff.predicate_diffs.push(DiffEntry::Added(name.clone()));
204 }
205 Some(a_pred) => {
206 let arity_changed = a_pred.arity != b_pred.arity;
207 let domains_changed = a_pred.arg_domains != b_pred.arg_domains;
208 if arity_changed || domains_changed {
209 diff.predicate_diffs.push(DiffEntry::Modified {
210 before: format!(
211 "{}(arity={}, domains=[{}])",
212 name,
213 a_pred.arity,
214 a_pred.arg_domains.join(", ")
215 ),
216 after: format!(
217 "{}(arity={}, domains=[{}])",
218 name,
219 b_pred.arity,
220 b_pred.arg_domains.join(", ")
221 ),
222 });
223 }
224 }
225 }
226 }
227 for name in a.predicates.keys() {
228 if !b.predicates.contains_key(name) {
229 diff.predicate_diffs.push(DiffEntry::Removed(name.clone()));
230 }
231 }
232
233 diff
234}
235
236#[cfg(test)]
239mod tests {
240 use super::*;
241 use tensorlogic_adapters::{DomainInfo, PredicateInfo};
242
243 fn make_table_with_domain(name: &str) -> SymbolTable {
244 let mut t = SymbolTable::new();
245 t.add_domain(DomainInfo::new(name, 10))
246 .expect("add_domain should succeed");
247 t
248 }
249
250 fn make_table_with_predicate(domain: &str, pred: &str) -> SymbolTable {
251 let mut t = SymbolTable::new();
252 t.add_domain(DomainInfo::new(domain, 10))
253 .expect("add_domain should succeed");
254 t.add_predicate(PredicateInfo::new(pred, vec![domain.to_string()]))
255 .expect("add_predicate should succeed");
256 t
257 }
258
259 #[test]
260 fn test_diff_identical_tables() {
261 let a = SymbolTable::new();
262 let b = SymbolTable::new();
263 let diff = compare_symbol_tables(&a, &b);
264 assert!(diff.is_empty());
265 }
266
267 #[test]
268 fn test_diff_added_domain() {
269 let a = SymbolTable::new();
270 let b = make_table_with_domain("Person");
271 let diff = compare_symbol_tables(&a, &b);
272 assert_eq!(diff.domain_diffs.len(), 1);
273 assert!(diff.domain_diffs[0].is_addition());
274 assert_eq!(diff.domain_diffs[0].name(), "Person");
275 }
276
277 #[test]
278 fn test_diff_removed_domain() {
279 let a = make_table_with_domain("Animal");
280 let b = SymbolTable::new();
281 let diff = compare_symbol_tables(&a, &b);
282 assert_eq!(diff.domain_diffs.len(), 1);
283 assert!(diff.domain_diffs[0].is_removal());
284 assert_eq!(diff.domain_diffs[0].name(), "Animal");
285 }
286
287 #[test]
288 fn test_diff_added_predicate() {
289 let a = make_table_with_domain("Person");
290 let b = make_table_with_predicate("Person", "knows");
291 let diff = compare_symbol_tables(&a, &b);
292 assert_eq!(diff.predicate_diffs.len(), 1);
293 assert!(diff.predicate_diffs[0].is_addition());
294 assert_eq!(diff.predicate_diffs[0].name(), "knows");
295 }
296
297 #[test]
298 fn test_diff_removed_predicate() {
299 let a = make_table_with_predicate("Person", "knows");
300 let b = make_table_with_domain("Person");
301 let diff = compare_symbol_tables(&a, &b);
302 assert_eq!(diff.predicate_diffs.len(), 1);
303 assert!(diff.predicate_diffs[0].is_removal());
304 assert_eq!(diff.predicate_diffs[0].name(), "knows");
305 }
306
307 #[test]
308 fn test_diff_is_empty_on_empty() {
309 assert!(OntologyDiff::new().is_empty());
310 }
311
312 #[test]
313 fn test_diff_total_changes() {
314 let mut diff = OntologyDiff::new();
315 diff.domain_diffs.push(DiffEntry::Added("A".to_string()));
316 diff.domain_diffs.push(DiffEntry::Added("B".to_string()));
317 diff.predicate_diffs
318 .push(DiffEntry::Removed("p".to_string()));
319 assert_eq!(diff.total_changes(), 3);
320 }
321
322 #[test]
323 fn test_diff_report_nonempty() {
324 let mut a = SymbolTable::new();
325 a.add_domain(DomainInfo::new("OldDomain", 5))
326 .expect("add_domain should succeed");
327 let mut b = SymbolTable::new();
328 b.add_domain(DomainInfo::new("NewDomain", 5))
329 .expect("add_domain should succeed");
330 let diff = compare_symbol_tables(&a, &b);
331 let report = diff.report();
332 assert!(report.contains("Added") || report.contains("Removed"));
333 }
334
335 #[test]
336 fn test_diff_summary_format() {
337 let diff = OntologyDiff::new();
338 let summary = diff.summary();
339 assert!(summary.starts_with("OntologyDiff:"));
340 }
341
342 #[test]
343 fn test_diff_entry_helpers() {
344 let added = DiffEntry::Added("x".to_string());
345 assert!(added.is_addition());
346 assert!(!added.is_removal());
347 assert!(!added.is_modification());
348
349 let removed = DiffEntry::Removed("y".to_string());
350 assert!(!removed.is_addition());
351 assert!(removed.is_removal());
352 assert!(!removed.is_modification());
353
354 let modified = DiffEntry::Modified {
355 before: "z(arity=1)".to_string(),
356 after: "z(arity=2)".to_string(),
357 };
358 assert!(!modified.is_addition());
359 assert!(!modified.is_removal());
360 assert!(modified.is_modification());
361 }
362}