use std::collections::{HashMap, HashSet, VecDeque};
use crate::error::{NopalError, Result};
use crate::index::{Index, IndexQuery};
use crate::types::{NodeId, PropertyValue};
#[derive(Debug, Default, Clone)]
pub struct TaxonomyIndex {
children: HashMap<NodeId, Vec<NodeId>>,
parents: HashMap<NodeId, Vec<NodeId>>,
labels: HashMap<NodeId, String>,
label_to_id: HashMap<String, NodeId>,
closure_cache: HashMap<NodeId, Option<HashSet<NodeId>>>,
}
impl TaxonomyIndex {
pub fn new() -> Self {
Self::default()
}
pub fn register_class(&mut self, id: NodeId, label: impl Into<String>) {
let label = label.into();
if let Some(old_label) = self.labels.get(&id)
&& old_label != &label
{
self.label_to_id.remove(old_label);
}
self.labels.insert(id, label.clone());
self.label_to_id.insert(label, id);
self.children.entry(id).or_default();
self.parents.entry(id).or_default();
}
pub fn add_subclass(&mut self, parent: NodeId, child: NodeId) -> Result<()> {
if parent == child {
return Err(NopalError::custom(
"add_subclass: self-loops are not allowed in a class hierarchy",
));
}
let children_of_parent = self.children.entry(parent).or_default();
if !children_of_parent.contains(&child) {
children_of_parent.push(child);
}
let parents_of_child = self.parents.entry(child).or_default();
if !parents_of_child.contains(&parent) {
parents_of_child.push(parent);
}
self.children.entry(child).or_default();
self.parents.entry(parent).or_default();
self.invalidate_cache_for(parent);
Ok(())
}
pub fn remove_subclass(&mut self, parent: NodeId, child: NodeId) -> Result<()> {
if let Some(children) = self.children.get_mut(&parent) {
children.retain(|&c| c != child);
}
if let Some(parents) = self.parents.get_mut(&child) {
parents.retain(|&p| p != parent);
}
self.invalidate_cache_for(parent);
Ok(())
}
fn invalidate_cache_for(&mut self, node: NodeId) {
let mut to_invalidate = vec![node];
let mut visited: HashSet<NodeId> = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(node);
while let Some(current) = queue.pop_front() {
if !visited.insert(current) {
continue;
}
to_invalidate.push(current);
if let Some(pars) = self.parents.get(¤t) {
for &p in pars {
if !visited.contains(&p) {
queue.push_back(p);
}
}
}
}
for id in to_invalidate {
self.closure_cache.insert(id, None);
}
}
pub fn descendants(&mut self, root: NodeId) -> Vec<NodeId> {
if let Some(Some(cached)) = self.closure_cache.get(&root) {
return cached.iter().copied().collect();
}
let mut visited: HashSet<NodeId> = HashSet::new();
let mut queue: VecDeque<NodeId> = VecDeque::new();
if let Some(direct) = self.children.get(&root) {
for &c in direct {
if c != root {
queue.push_back(c);
}
}
}
while let Some(current) = queue.pop_front() {
if !visited.insert(current) {
continue;
}
if let Some(kids) = self.children.get(¤t) {
for &kid in kids {
if !visited.contains(&kid) {
queue.push_back(kid);
}
}
}
}
self.closure_cache.insert(root, Some(visited.clone()));
visited.into_iter().collect()
}
pub fn ancestors(&self, node: NodeId) -> Vec<NodeId> {
let mut visited: HashSet<NodeId> = HashSet::new();
let mut queue: VecDeque<NodeId> = VecDeque::new();
if let Some(direct) = self.parents.get(&node) {
for &p in direct {
if p != node {
queue.push_back(p);
}
}
}
while let Some(current) = queue.pop_front() {
if !visited.insert(current) {
continue;
}
if let Some(pars) = self.parents.get(¤t) {
for &p in pars {
if !visited.contains(&p) {
queue.push_back(p);
}
}
}
}
visited.into_iter().collect()
}
pub fn is_subclass_of(&mut self, child: NodeId, ancestor: NodeId) -> bool {
if child == ancestor {
return false;
}
self.descendants(ancestor).contains(&child)
}
pub fn find_by_label(&self, label: &str) -> Option<NodeId> {
self.label_to_id.get(label).copied()
}
pub fn is_subclass_of_label(&mut self, child_label: &str, ancestor_id: NodeId) -> bool {
match self.find_by_label(child_label) {
Some(child_id) => self.is_subclass_of(child_id, ancestor_id),
None => false,
}
}
pub fn direct_children(&self, parent: NodeId) -> Vec<NodeId> {
self.children
.get(&parent)
.cloned()
.unwrap_or_default()
}
pub fn direct_parents(&self, child: NodeId) -> Vec<NodeId> {
self.parents
.get(&child)
.cloned()
.unwrap_or_default()
}
pub fn size(&self) -> usize {
self.labels.len()
}
pub fn all_class_ids(&self) -> Vec<NodeId> {
self.labels.keys().copied().collect()
}
pub fn edge_count(&self) -> usize {
self.children.values().map(|v| v.len()).sum()
}
pub fn clear(&mut self) {
self.children.clear();
self.parents.clear();
self.labels.clear();
self.label_to_id.clear();
self.closure_cache.clear();
}
fn unregister_class(&mut self, id: NodeId) {
if let Some(label) = self.labels.remove(&id) {
self.label_to_id.remove(&label);
}
if let Some(parent_ids) = self.parents.remove(&id) {
for parent in &parent_ids {
if let Some(kids) = self.children.get_mut(parent) {
kids.retain(|&k| k != id);
}
}
}
if let Some(child_ids) = self.children.remove(&id) {
for child in &child_ids {
if let Some(pars) = self.parents.get_mut(child) {
pars.retain(|&p| p != id);
}
}
}
self.invalidate_cache_for(id);
self.closure_cache.remove(&id);
}
}
impl Index for TaxonomyIndex {
fn insert(&mut self, value: PropertyValue, node_id: NodeId) -> Result<()> {
if let PropertyValue::String(label) = value {
self.register_class(node_id, label);
}
Ok(())
}
fn remove(&mut self, _value: &PropertyValue, node_id: NodeId) -> Result<()> {
self.unregister_class(node_id);
Ok(())
}
fn query(&self, query: &IndexQuery) -> Result<Vec<NodeId>> {
match query {
IndexQuery::Equals(PropertyValue::String(label)) => {
Ok(self.find_by_label(label).map(|id| vec![id]).unwrap_or_default())
}
_ => Err(NopalError::custom(
"TaxonomyIndex only supports IndexQuery::Equals(String(label))",
)),
}
}
fn clear(&mut self) -> Result<()> {
TaxonomyIndex::clear(self);
Ok(())
}
fn size(&self) -> usize {
TaxonomyIndex::size(self)
}
fn add_relationship(&mut self, source: NodeId, target: NodeId) -> Result<()> {
self.add_subclass(source, target)
}
fn as_taxonomy(&self) -> Option<&TaxonomyIndex> {
Some(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
fn id() -> NodeId {
Uuid::new_v4()
}
#[test]
fn test_register_class() {
let mut idx = TaxonomyIndex::new();
let animal = id();
idx.register_class(animal, "Animal");
assert_eq!(idx.size(), 1);
assert_eq!(idx.find_by_label("Animal"), Some(animal));
assert_eq!(idx.find_by_label("Unknown"), None);
}
#[test]
fn test_add_subclass_simple() {
let mut idx = TaxonomyIndex::new();
let animal = id();
let mammal = id();
idx.register_class(animal, "Animal");
idx.register_class(mammal, "Mammal");
idx.add_subclass(animal, mammal).unwrap();
let desc = idx.descendants(animal);
assert_eq!(desc.len(), 1);
assert!(desc.contains(&mammal));
}
#[test]
fn test_transitive_closure() {
let mut idx = TaxonomyIndex::new();
let animal = id();
let mammal = id();
let dog = id();
idx.register_class(animal, "Animal");
idx.register_class(mammal, "Mammal");
idx.register_class(dog, "Dog");
idx.add_subclass(animal, mammal).unwrap();
idx.add_subclass(mammal, dog).unwrap();
let desc = idx.descendants(animal);
assert_eq!(desc.len(), 2);
assert!(desc.contains(&mammal));
assert!(desc.contains(&dog));
}
#[test]
fn test_is_subclass_of() {
let mut idx = TaxonomyIndex::new();
let animal = id();
let mammal = id();
let dog = id();
idx.register_class(animal, "Animal");
idx.register_class(mammal, "Mammal");
idx.register_class(dog, "Dog");
idx.add_subclass(animal, mammal).unwrap();
idx.add_subclass(mammal, dog).unwrap();
assert!(idx.is_subclass_of(dog, animal));
assert!(idx.is_subclass_of(dog, mammal));
assert!(idx.is_subclass_of(mammal, animal));
assert!(!idx.is_subclass_of(animal, dog));
assert!(!idx.is_subclass_of(dog, dog));
}
#[test]
fn test_remove_subclass() {
let mut idx = TaxonomyIndex::new();
let animal = id();
let mammal = id();
let dog = id();
idx.register_class(animal, "Animal");
idx.register_class(mammal, "Mammal");
idx.register_class(dog, "Dog");
idx.add_subclass(animal, mammal).unwrap();
idx.add_subclass(mammal, dog).unwrap();
idx.remove_subclass(mammal, dog).unwrap();
let desc_animal = idx.descendants(animal);
assert!(desc_animal.contains(&mammal));
assert!(!desc_animal.contains(&dog));
let desc_mammal = idx.descendants(mammal);
assert!(desc_mammal.is_empty());
}
#[test]
fn test_ancestors() {
let mut idx = TaxonomyIndex::new();
let animal = id();
let mammal = id();
let dog = id();
idx.register_class(animal, "Animal");
idx.register_class(mammal, "Mammal");
idx.register_class(dog, "Dog");
idx.add_subclass(animal, mammal).unwrap();
idx.add_subclass(mammal, dog).unwrap();
let anc = idx.ancestors(dog);
assert_eq!(anc.len(), 2);
assert!(anc.contains(&mammal));
assert!(anc.contains(&animal));
let anc_animal = idx.ancestors(animal);
assert!(anc_animal.is_empty());
}
#[test]
fn test_direct_children() {
let mut idx = TaxonomyIndex::new();
let animal = id();
let mammal = id();
let dog = id();
idx.register_class(animal, "Animal");
idx.register_class(mammal, "Mammal");
idx.register_class(dog, "Dog");
idx.add_subclass(animal, mammal).unwrap();
idx.add_subclass(mammal, dog).unwrap();
let dc = idx.direct_children(animal);
assert_eq!(dc.len(), 1);
assert!(dc.contains(&mammal));
assert!(!dc.contains(&dog)); }
#[test]
fn test_diamond_hierarchy() {
let mut idx = TaxonomyIndex::new();
let a = id();
let b = id();
let c = id();
let d = id();
idx.register_class(a, "A");
idx.register_class(b, "B");
idx.register_class(c, "C");
idx.register_class(d, "D");
idx.add_subclass(a, b).unwrap();
idx.add_subclass(a, c).unwrap();
idx.add_subclass(b, d).unwrap();
idx.add_subclass(c, d).unwrap();
let desc_a = idx.descendants(a);
assert_eq!(desc_a.len(), 3, "A should have B, C, D as descendants");
assert!(desc_a.contains(&b));
assert!(desc_a.contains(&c));
assert!(desc_a.contains(&d));
let anc_d = idx.ancestors(d);
assert_eq!(anc_d.len(), 3);
assert!(anc_d.contains(&a));
assert!(anc_d.contains(&b));
assert!(anc_d.contains(&c));
assert!(idx.is_subclass_of(d, a));
}
#[test]
fn test_cache_invalidation() {
let mut idx = TaxonomyIndex::new();
let animal = id();
let mammal = id();
let dog = id();
idx.register_class(animal, "Animal");
idx.register_class(mammal, "Mammal");
idx.register_class(dog, "Dog");
idx.add_subclass(animal, mammal).unwrap();
let desc = idx.descendants(animal);
assert_eq!(desc.len(), 1);
assert!(desc.contains(&mammal));
idx.add_subclass(mammal, dog).unwrap();
let desc2 = idx.descendants(animal);
assert_eq!(desc2.len(), 2, "cache should be invalidated after add_subclass");
assert!(desc2.contains(&dog));
idx.remove_subclass(mammal, dog).unwrap();
let desc3 = idx.descendants(animal);
assert_eq!(desc3.len(), 1, "cache should be re-invalidated after remove_subclass");
assert!(!desc3.contains(&dog));
}
#[test]
fn test_self_loop_rejected() {
let mut idx = TaxonomyIndex::new();
let animal = id();
idx.register_class(animal, "Animal");
let result = idx.add_subclass(animal, animal);
assert!(result.is_err());
}
#[test]
fn test_idempotent_add_subclass() {
let mut idx = TaxonomyIndex::new();
let a = id();
let b = id();
idx.register_class(a, "A");
idx.register_class(b, "B");
idx.add_subclass(a, b).unwrap();
idx.add_subclass(a, b).unwrap();
assert_eq!(idx.direct_children(a).len(), 1);
assert_eq!(idx.direct_parents(b).len(), 1);
}
#[test]
fn test_clear() {
let mut idx = TaxonomyIndex::new();
let a = id();
let b = id();
idx.register_class(a, "A");
idx.register_class(b, "B");
idx.add_subclass(a, b).unwrap();
idx.clear();
assert_eq!(idx.size(), 0);
assert!(idx.find_by_label("A").is_none());
assert!(idx.direct_children(a).is_empty());
assert!(idx.descendants(a).is_empty());
}
#[test]
fn test_index_trait_insert() {
use crate::index::{Index, IndexQuery};
let mut idx = TaxonomyIndex::new();
let animal = id();
idx.insert(PropertyValue::String("Animal".to_string()), animal).unwrap();
assert_eq!(idx.size(), 1);
let result = idx.query(&IndexQuery::Equals(PropertyValue::String("Animal".to_string()))).unwrap();
assert_eq!(result, vec![animal]);
let result2 = idx.query(&IndexQuery::Equals(PropertyValue::String("Unknown".to_string()))).unwrap();
assert!(result2.is_empty());
}
#[test]
fn test_index_trait_remove() {
use crate::index::Index;
let mut idx = TaxonomyIndex::new();
let animal = id();
let mammal = id();
idx.insert(PropertyValue::String("Animal".to_string()), animal).unwrap();
idx.insert(PropertyValue::String("Mammal".to_string()), mammal).unwrap();
idx.add_subclass(animal, mammal).unwrap();
assert_eq!(idx.size(), 2);
idx.remove(&PropertyValue::String("Mammal".to_string()), mammal).unwrap();
assert_eq!(idx.size(), 1);
assert!(idx.direct_children(animal).is_empty());
assert!(idx.find_by_label("Mammal").is_none());
}
#[test]
fn test_index_trait_add_relationship() {
use crate::index::Index;
let mut idx = TaxonomyIndex::new();
let animal = id();
let mammal = id();
let dog = id();
idx.insert(PropertyValue::String("Animal".to_string()), animal).unwrap();
idx.insert(PropertyValue::String("Mammal".to_string()), mammal).unwrap();
idx.insert(PropertyValue::String("Dog".to_string()), dog).unwrap();
idx.add_relationship(animal, mammal).unwrap();
idx.add_relationship(mammal, dog).unwrap();
assert!(idx.is_subclass_of(dog, animal));
assert!(idx.is_subclass_of(mammal, animal));
assert!(!idx.is_subclass_of(animal, dog));
}
#[test]
fn test_unregister_class() {
let mut idx = TaxonomyIndex::new();
let animal = id();
let mammal = id();
let dog = id();
idx.register_class(animal, "Animal");
idx.register_class(mammal, "Mammal");
idx.register_class(dog, "Dog");
idx.add_subclass(animal, mammal).unwrap();
idx.add_subclass(mammal, dog).unwrap();
idx.unregister_class(mammal);
assert_eq!(idx.size(), 2);
assert!(idx.find_by_label("Mammal").is_none());
assert!(idx.direct_children(animal).is_empty());
assert!(idx.direct_parents(dog).is_empty());
let desc = idx.descendants(animal);
assert!(desc.is_empty());
}
#[test]
fn test_index_trait_query_non_string_errors() {
use crate::index::{Index, IndexQuery};
let idx = TaxonomyIndex::new();
let result = idx.query(&IndexQuery::Equals(PropertyValue::Int(42)));
assert!(result.is_err());
let result2 = idx.query(&IndexQuery::GreaterThan(PropertyValue::Int(1)));
assert!(result2.is_err());
}
}