#[cfg(not(feature = "std"))]
use alloc::string::{String, ToString};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use crate::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EntityDescriptor {
pub name: String,
pub entity_type: String,
pub attributes: Vec<(String, String)>,
#[cfg_attr(feature = "serde", serde(default))]
pub relations: Vec<(String, String)>,
}
impl EntityDescriptor {
pub fn new(name: impl Into<String>, entity_type: impl Into<String>) -> Self {
Self {
name: name.into(),
entity_type: entity_type.into(),
attributes: Vec::new(),
relations: Vec::new(),
}
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.push((key.into(), value.into()));
self
}
pub fn with_relation(mut self, label: impl Into<String>, target: impl Into<String>) -> Self {
self.relations.push((label.into(), target.into()));
self
}
pub fn attribute(&self, key: &str) -> Option<&str> {
self.attributes
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
pub fn relation(&self, label: &str) -> Option<&str> {
self.relations
.iter()
.find(|(l, _)| l == label)
.map(|(_, t)| t.as_str())
}
}
#[derive(Debug, Clone, Default)]
pub struct EntityRegistry {
entries: HashMap<(String, String), EntityDescriptor>,
}
impl EntityRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, descriptor: EntityDescriptor) {
let key = (descriptor.entity_type.clone(), descriptor.name.clone());
self.entries.insert(key, descriptor);
}
pub fn get(&self, entity_type: &str, name: &str) -> Option<&EntityDescriptor> {
self.entries
.get(&(entity_type.to_string(), name.to_string()))
}
pub fn iter(&self) -> impl Iterator<Item = &EntityDescriptor> {
self.entries.values()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SubgraphDescription {
pub attributes: Vec<String>,
pub relation: Option<(String, String)>,
}
pub(crate) fn incremental_attributes_with_remaining<'a>(
target: &EntityDescriptor,
registry: &'a EntityRegistry,
preference_order: &[String],
) -> (Vec<String>, Vec<&'a EntityDescriptor>) {
let mut distractors: Vec<&EntityDescriptor> = registry
.iter()
.filter(|d| d.name != target.name && d.entity_type == target.entity_type)
.collect();
if distractors.is_empty() {
return (Vec::new(), Vec::new());
}
let mut walked: Vec<&String> = preference_order.iter().collect();
for (k, _) in &target.attributes {
if !walked.iter().any(|s| s.as_str() == k.as_str()) {
walked.push(k);
}
}
let mut chosen: Vec<String> = Vec::new();
for attr_key in walked {
if distractors.is_empty() {
break;
}
let target_value = match target.attribute(attr_key) {
Some(v) => v,
None => continue,
};
let still_matching: Vec<&EntityDescriptor> = distractors
.iter()
.copied()
.filter(|d| d.attribute(attr_key) == Some(target_value))
.collect();
if still_matching.len() < distractors.len() {
chosen.push(target_value.to_string());
distractors = still_matching;
}
}
(chosen, distractors)
}
pub fn distinguishing_attributes(
target: &EntityDescriptor,
registry: &EntityRegistry,
preference_order: &[String],
) -> Vec<String> {
let (attrs, _) = incremental_attributes_with_remaining(target, registry, preference_order);
attrs
}
pub fn distinguishing_subgraph(
target: &EntityDescriptor,
registry: &EntityRegistry,
preference_order: &[String],
) -> SubgraphDescription {
let (attrs, remaining) =
incremental_attributes_with_remaining(target, registry, preference_order);
if remaining.is_empty() {
return SubgraphDescription {
attributes: attrs,
relation: None,
};
}
for (label, target_name) in &target.relations {
let any_shared = remaining.iter().any(|d| {
d.relations
.iter()
.any(|(l, t)| l == label && t == target_name)
});
if !any_shared {
return SubgraphDescription {
attributes: attrs,
relation: Some((label.clone(), target_name.clone())),
};
}
}
SubgraphDescription {
attributes: attrs,
relation: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reg_with(entities: Vec<EntityDescriptor>) -> EntityRegistry {
let mut r = EntityRegistry::new();
for e in entities {
r.insert(e);
}
r
}
#[test]
fn no_distractors_yields_empty_attribute_list() {
let target =
EntityDescriptor::new("UserService", "class").with_attribute("layer", "domain");
let registry = reg_with(vec![target.clone()]);
let attrs = distinguishing_attributes(&target, ®istry, &[]);
assert!(attrs.is_empty());
}
#[test]
fn different_type_distractor_does_not_force_attribute() {
let target =
EntityDescriptor::new("UserService", "class").with_attribute("layer", "domain");
let other = EntityDescriptor::new("UserService", "trait").with_attribute("layer", "infra");
let registry = reg_with(vec![target.clone(), other]);
let attrs = distinguishing_attributes(&target, ®istry, &[]);
assert!(attrs.is_empty());
}
#[test]
fn same_type_requires_distinguishing_attribute() {
let target =
EntityDescriptor::new("UserService", "class").with_attribute("layer", "domain");
let distractor =
EntityDescriptor::new("AuthService", "class").with_attribute("layer", "infra");
let registry = reg_with(vec![target.clone(), distractor]);
let attrs = distinguishing_attributes(&target, ®istry, &[]);
assert_eq!(attrs, vec!["domain".to_string()]);
}
#[test]
fn preference_order_is_respected() {
let target = EntityDescriptor::new("Foo", "class")
.with_attribute("color", "red")
.with_attribute("size", "small");
let d1 = EntityDescriptor::new("Bar", "class")
.with_attribute("color", "blue")
.with_attribute("size", "small");
let registry = reg_with(vec![target.clone(), d1]);
let attrs = distinguishing_attributes(
&target,
®istry,
&["size".to_string(), "color".to_string()],
);
assert_eq!(attrs, vec!["red".to_string()]);
}
#[test]
fn preferred_attribute_selected_when_sufficient() {
let target = EntityDescriptor::new("Foo", "widget")
.with_attribute("color", "red")
.with_attribute("size", "small");
let d1 = EntityDescriptor::new("Bar", "widget")
.with_attribute("color", "blue")
.with_attribute("size", "large");
let registry = reg_with(vec![target.clone(), d1]);
let attrs = distinguishing_attributes(&target, ®istry, &["color".to_string()]);
assert_eq!(attrs, vec!["red".to_string()]);
}
#[test]
fn multiple_attributes_needed_for_full_disambiguation() {
let target = EntityDescriptor::new("Foo", "widget")
.with_attribute("color", "red")
.with_attribute("size", "small");
let same_color = EntityDescriptor::new("Bar", "widget")
.with_attribute("color", "red")
.with_attribute("size", "large");
let same_size = EntityDescriptor::new("Baz", "widget")
.with_attribute("color", "blue")
.with_attribute("size", "small");
let registry = reg_with(vec![target.clone(), same_color, same_size]);
let attrs = distinguishing_attributes(
&target,
®istry,
&["color".to_string(), "size".to_string()],
);
assert_eq!(attrs, vec!["red".to_string(), "small".to_string()]);
}
#[test]
fn useless_attribute_is_skipped() {
let target = EntityDescriptor::new("Foo", "widget")
.with_attribute("color", "red")
.with_attribute("size", "small");
let d1 = EntityDescriptor::new("Bar", "widget")
.with_attribute("color", "red")
.with_attribute("size", "large");
let d2 = EntityDescriptor::new("Baz", "widget")
.with_attribute("color", "red")
.with_attribute("size", "medium");
let registry = reg_with(vec![target.clone(), d1, d2]);
let attrs = distinguishing_attributes(
&target,
®istry,
&["color".to_string(), "size".to_string()],
);
assert_eq!(attrs, vec!["small".to_string()]);
}
#[test]
fn stops_as_soon_as_unambiguous() {
let target = EntityDescriptor::new("Foo", "widget")
.with_attribute("color", "red")
.with_attribute("size", "small")
.with_attribute("shape", "round");
let d1 = EntityDescriptor::new("Bar", "widget").with_attribute("color", "blue");
let registry = reg_with(vec![target.clone(), d1]);
let attrs = distinguishing_attributes(
&target,
®istry,
&["color".to_string(), "size".to_string(), "shape".to_string()],
);
assert_eq!(attrs, vec!["red".to_string()]);
}
#[test]
fn falls_back_to_registration_order_when_no_preference() {
let target = EntityDescriptor::new("Foo", "widget")
.with_attribute("first_attr", "A")
.with_attribute("second_attr", "B");
let d1 = EntityDescriptor::new("Bar", "widget")
.with_attribute("first_attr", "X")
.with_attribute("second_attr", "B");
let registry = reg_with(vec![target.clone(), d1]);
let attrs = distinguishing_attributes(&target, ®istry, &[]);
assert_eq!(attrs, vec!["A".to_string()]);
}
#[test]
fn missing_attribute_on_target_skips_without_panic() {
let target = EntityDescriptor::new("Foo", "widget").with_attribute("size", "small");
let d1 = EntityDescriptor::new("Bar", "widget").with_attribute("size", "large");
let registry = reg_with(vec![target.clone(), d1]);
let attrs = distinguishing_attributes(
&target,
®istry,
&["color".to_string(), "size".to_string()],
);
assert_eq!(attrs, vec!["small".to_string()]);
}
#[test]
fn registry_insert_replaces_same_type_and_name() {
let mut r = EntityRegistry::new();
r.insert(EntityDescriptor::new("X", "t").with_attribute("a", "1"));
r.insert(EntityDescriptor::new("X", "t").with_attribute("a", "2"));
assert_eq!(r.get("t", "X").unwrap().attribute("a"), Some("2"));
assert_eq!(r.len(), 1);
}
#[test]
fn registry_keeps_same_name_different_type_as_separate_entries() {
let mut r = EntityRegistry::new();
r.insert(EntityDescriptor::new("UserService", "class").with_attribute("a", "1"));
r.insert(EntityDescriptor::new("UserService", "trait").with_attribute("a", "2"));
assert_eq!(r.len(), 2);
assert_eq!(
r.get("class", "UserService").unwrap().attribute("a"),
Some("1")
);
assert_eq!(
r.get("trait", "UserService").unwrap().attribute("a"),
Some("2")
);
}
#[test]
fn with_relation_adds_edge() {
let e = EntityDescriptor::new("Handler", "function").with_relation("calls", "AuthService");
assert_eq!(
e.relations,
vec![("calls".to_string(), "AuthService".to_string())]
);
}
#[test]
fn relation_lookup_by_label() {
let e = EntityDescriptor::new("Handler", "function")
.with_relation("calls", "AuthService")
.with_relation("tests", "HandlerTests");
assert_eq!(e.relation("calls"), Some("AuthService"));
assert_eq!(e.relation("tests"), Some("HandlerTests"));
assert_eq!(e.relation("unknown"), None);
}
#[test]
fn default_has_empty_relations() {
let e = EntityDescriptor::default();
assert!(e.relations.is_empty());
}
#[test]
fn graph_reg_no_distractors_returns_empty() {
let target = EntityDescriptor::new("Foo", "class");
let registry = reg_with(vec![target.clone()]);
let desc = distinguishing_subgraph(&target, ®istry, &[]);
assert!(desc.attributes.is_empty());
assert!(desc.relation.is_none());
}
#[test]
fn graph_reg_falls_back_to_dale_reiter_when_attributes_suffice() {
let target =
EntityDescriptor::new("UserService", "class").with_attribute("layer", "domain");
let other = EntityDescriptor::new("AuthService", "class").with_attribute("layer", "infra");
let registry = reg_with(vec![target.clone(), other]);
let desc = distinguishing_subgraph(&target, ®istry, &[]);
assert_eq!(desc.attributes, vec!["domain".to_string()]);
assert!(desc.relation.is_none());
}
#[test]
fn graph_reg_adds_relation_when_attributes_dont_disambiguate() {
let target = EntityDescriptor::new("LoginHandler", "function")
.with_attribute("layer", "api")
.with_relation("calls", "AuthService");
let other = EntityDescriptor::new("LogoutHandler", "function")
.with_attribute("layer", "api")
.with_relation("calls", "SessionService");
let registry = reg_with(vec![target.clone(), other]);
let desc = distinguishing_subgraph(&target, ®istry, &[]);
assert_eq!(
desc.relation,
Some(("calls".to_string(), "AuthService".to_string()))
);
}
#[test]
fn graph_reg_skips_shared_relation_picks_next() {
let target = EntityDescriptor::new("LoginHandler", "function")
.with_relation("calls", "LogService")
.with_relation("tests", "LoginTests");
let other = EntityDescriptor::new("LogoutHandler", "function")
.with_relation("calls", "LogService")
.with_relation("tests", "LogoutTests");
let registry = reg_with(vec![target.clone(), other]);
let desc = distinguishing_subgraph(&target, ®istry, &[]);
assert_eq!(
desc.relation,
Some(("tests".to_string(), "LoginTests".to_string()))
);
}
#[test]
fn graph_reg_gives_up_when_nothing_distinguishes() {
let target = EntityDescriptor::new("Foo", "thing").with_relation("calls", "X");
let other = EntityDescriptor::new("Bar", "thing").with_relation("calls", "X");
let registry = reg_with(vec![target.clone(), other]);
let desc = distinguishing_subgraph(&target, ®istry, &[]);
assert!(desc.relation.is_none());
}
#[test]
fn graph_reg_combines_attributes_and_relation() {
let target = EntityDescriptor::new("LoginHandler", "function")
.with_attribute("layer", "api")
.with_relation("calls", "AuthService");
let d1 = EntityDescriptor::new("LogoutHandler", "function")
.with_attribute("layer", "api")
.with_relation("calls", "SessionService");
let d2 = EntityDescriptor::new("ProfileHandler", "function")
.with_attribute("layer", "web")
.with_relation("calls", "AuthService");
let registry = reg_with(vec![target.clone(), d1, d2]);
let desc = distinguishing_subgraph(&target, ®istry, &[]);
assert_eq!(desc.attributes, vec!["api".to_string()]);
assert_eq!(
desc.relation,
Some(("calls".to_string(), "AuthService".to_string()))
);
}
}