use serde::{Deserialize, Serialize};
use tensorlogic_adapters::SymbolTable;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DiffEntry {
Added(String),
Removed(String),
Modified { before: String, after: String },
}
impl DiffEntry {
pub fn name(&self) -> &str {
match self {
DiffEntry::Added(n) => n.as_str(),
DiffEntry::Removed(n) => n.as_str(),
DiffEntry::Modified { before, .. } => before.as_str(),
}
}
pub fn is_addition(&self) -> bool {
matches!(self, DiffEntry::Added(_))
}
pub fn is_removal(&self) -> bool {
matches!(self, DiffEntry::Removed(_))
}
pub fn is_modification(&self) -> bool {
matches!(self, DiffEntry::Modified { .. })
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OntologyDiff {
pub domain_diffs: Vec<DiffEntry>,
pub predicate_diffs: Vec<DiffEntry>,
}
impl OntologyDiff {
pub fn new() -> Self {
OntologyDiff::default()
}
pub fn is_empty(&self) -> bool {
self.domain_diffs.is_empty() && self.predicate_diffs.is_empty()
}
pub fn total_changes(&self) -> usize {
self.domain_diffs.len() + self.predicate_diffs.len()
}
pub fn report(&self) -> String {
let mut out = String::new();
out.push_str("OntologyDiff Report\n");
out.push_str("===================\n");
if self.domain_diffs.is_empty() {
out.push_str("Domains: no changes\n");
} else {
out.push_str("Domains:\n");
for entry in &self.domain_diffs {
match entry {
DiffEntry::Added(n) => {
out.push_str(&format!(" + Added: {}\n", n));
}
DiffEntry::Removed(n) => {
out.push_str(&format!(" - Removed: {}\n", n));
}
DiffEntry::Modified { before, after } => {
out.push_str(&format!(" ~ Modified: {} -> {}\n", before, after));
}
}
}
}
if self.predicate_diffs.is_empty() {
out.push_str("Predicates: no changes\n");
} else {
out.push_str("Predicates:\n");
for entry in &self.predicate_diffs {
match entry {
DiffEntry::Added(n) => {
out.push_str(&format!(" + Added: {}\n", n));
}
DiffEntry::Removed(n) => {
out.push_str(&format!(" - Removed: {}\n", n));
}
DiffEntry::Modified { before, after } => {
out.push_str(&format!(" ~ Modified: {} -> {}\n", before, after));
}
}
}
}
out
}
pub fn summary(&self) -> String {
let added = self.domain_diffs.iter().filter(|e| e.is_addition()).count()
+ self
.predicate_diffs
.iter()
.filter(|e| e.is_addition())
.count();
let removed = self.domain_diffs.iter().filter(|e| e.is_removal()).count()
+ self
.predicate_diffs
.iter()
.filter(|e| e.is_removal())
.count();
let modified = self
.domain_diffs
.iter()
.filter(|e| e.is_modification())
.count()
+ self
.predicate_diffs
.iter()
.filter(|e| e.is_modification())
.count();
format!(
"OntologyDiff: {} added, {} removed, {} modified ({} total)",
added,
removed,
modified,
self.total_changes()
)
}
}
pub fn compare_symbol_tables(a: &SymbolTable, b: &SymbolTable) -> OntologyDiff {
let mut diff = OntologyDiff::new();
for (name, b_info) in &b.domains {
match a.domains.get(name) {
None => {
diff.domain_diffs.push(DiffEntry::Added(name.clone()));
}
Some(a_info) => {
if a_info.cardinality != b_info.cardinality {
diff.domain_diffs.push(DiffEntry::Modified {
before: format!("{}(cardinality={})", name, a_info.cardinality),
after: format!("{}(cardinality={})", name, b_info.cardinality),
});
}
}
}
}
for name in a.domains.keys() {
if !b.domains.contains_key(name) {
diff.domain_diffs.push(DiffEntry::Removed(name.clone()));
}
}
for (name, b_pred) in &b.predicates {
match a.predicates.get(name) {
None => {
diff.predicate_diffs.push(DiffEntry::Added(name.clone()));
}
Some(a_pred) => {
let arity_changed = a_pred.arity != b_pred.arity;
let domains_changed = a_pred.arg_domains != b_pred.arg_domains;
if arity_changed || domains_changed {
diff.predicate_diffs.push(DiffEntry::Modified {
before: format!(
"{}(arity={}, domains=[{}])",
name,
a_pred.arity,
a_pred.arg_domains.join(", ")
),
after: format!(
"{}(arity={}, domains=[{}])",
name,
b_pred.arity,
b_pred.arg_domains.join(", ")
),
});
}
}
}
}
for name in a.predicates.keys() {
if !b.predicates.contains_key(name) {
diff.predicate_diffs.push(DiffEntry::Removed(name.clone()));
}
}
diff
}
#[cfg(test)]
mod tests {
use super::*;
use tensorlogic_adapters::{DomainInfo, PredicateInfo};
fn make_table_with_domain(name: &str) -> SymbolTable {
let mut t = SymbolTable::new();
t.add_domain(DomainInfo::new(name, 10))
.expect("add_domain should succeed");
t
}
fn make_table_with_predicate(domain: &str, pred: &str) -> SymbolTable {
let mut t = SymbolTable::new();
t.add_domain(DomainInfo::new(domain, 10))
.expect("add_domain should succeed");
t.add_predicate(PredicateInfo::new(pred, vec![domain.to_string()]))
.expect("add_predicate should succeed");
t
}
#[test]
fn test_diff_identical_tables() {
let a = SymbolTable::new();
let b = SymbolTable::new();
let diff = compare_symbol_tables(&a, &b);
assert!(diff.is_empty());
}
#[test]
fn test_diff_added_domain() {
let a = SymbolTable::new();
let b = make_table_with_domain("Person");
let diff = compare_symbol_tables(&a, &b);
assert_eq!(diff.domain_diffs.len(), 1);
assert!(diff.domain_diffs[0].is_addition());
assert_eq!(diff.domain_diffs[0].name(), "Person");
}
#[test]
fn test_diff_removed_domain() {
let a = make_table_with_domain("Animal");
let b = SymbolTable::new();
let diff = compare_symbol_tables(&a, &b);
assert_eq!(diff.domain_diffs.len(), 1);
assert!(diff.domain_diffs[0].is_removal());
assert_eq!(diff.domain_diffs[0].name(), "Animal");
}
#[test]
fn test_diff_added_predicate() {
let a = make_table_with_domain("Person");
let b = make_table_with_predicate("Person", "knows");
let diff = compare_symbol_tables(&a, &b);
assert_eq!(diff.predicate_diffs.len(), 1);
assert!(diff.predicate_diffs[0].is_addition());
assert_eq!(diff.predicate_diffs[0].name(), "knows");
}
#[test]
fn test_diff_removed_predicate() {
let a = make_table_with_predicate("Person", "knows");
let b = make_table_with_domain("Person");
let diff = compare_symbol_tables(&a, &b);
assert_eq!(diff.predicate_diffs.len(), 1);
assert!(diff.predicate_diffs[0].is_removal());
assert_eq!(diff.predicate_diffs[0].name(), "knows");
}
#[test]
fn test_diff_is_empty_on_empty() {
assert!(OntologyDiff::new().is_empty());
}
#[test]
fn test_diff_total_changes() {
let mut diff = OntologyDiff::new();
diff.domain_diffs.push(DiffEntry::Added("A".to_string()));
diff.domain_diffs.push(DiffEntry::Added("B".to_string()));
diff.predicate_diffs
.push(DiffEntry::Removed("p".to_string()));
assert_eq!(diff.total_changes(), 3);
}
#[test]
fn test_diff_report_nonempty() {
let mut a = SymbolTable::new();
a.add_domain(DomainInfo::new("OldDomain", 5))
.expect("add_domain should succeed");
let mut b = SymbolTable::new();
b.add_domain(DomainInfo::new("NewDomain", 5))
.expect("add_domain should succeed");
let diff = compare_symbol_tables(&a, &b);
let report = diff.report();
assert!(report.contains("Added") || report.contains("Removed"));
}
#[test]
fn test_diff_summary_format() {
let diff = OntologyDiff::new();
let summary = diff.summary();
assert!(summary.starts_with("OntologyDiff:"));
}
#[test]
fn test_diff_entry_helpers() {
let added = DiffEntry::Added("x".to_string());
assert!(added.is_addition());
assert!(!added.is_removal());
assert!(!added.is_modification());
let removed = DiffEntry::Removed("y".to_string());
assert!(!removed.is_addition());
assert!(removed.is_removal());
assert!(!removed.is_modification());
let modified = DiffEntry::Modified {
before: "z(arity=1)".to_string(),
after: "z(arity=2)".to_string(),
};
assert!(!modified.is_addition());
assert!(!modified.is_removal());
assert!(modified.is_modification());
}
}