use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntityProperty {
pub name: String,
pub data_type: String,
pub optional: bool,
pub description: String,
}
impl EntityProperty {
pub fn new(
name: impl Into<String>,
data_type: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
name: name.into(),
data_type: data_type.into(),
optional: false,
description: description.into(),
}
}
pub fn optional(
name: impl Into<String>,
data_type: impl Into<String>,
description: impl Into<String>,
) -> Self {
Self {
name: name.into(),
data_type: data_type.into(),
optional: true,
description: description.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntityDefinition {
pub urn: String,
pub name: String,
pub description: Option<String>,
pub extends: Option<String>,
pub is_abstract: bool,
pub properties: Vec<EntityProperty>,
}
impl EntityDefinition {
pub fn new(urn: impl Into<String>, name: impl Into<String>) -> Self {
Self {
urn: urn.into(),
name: name.into(),
description: None,
extends: None,
is_abstract: false,
properties: Vec::new(),
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_extends(mut self, parent_urn: impl Into<String>) -> Self {
self.extends = Some(parent_urn.into());
self
}
pub fn as_abstract(mut self) -> Self {
self.is_abstract = true;
self
}
pub fn with_property(mut self, prop: EntityProperty) -> Self {
self.properties.push(prop);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EntityResolverError {
NotFound(String),
CircularReference {
cycle: Vec<String>,
},
AbstractEntity(String),
DuplicateUrn(String),
ResolutionError(String),
}
impl std::fmt::Display for EntityResolverError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(urn) => write!(f, "entity not found: {urn}"),
Self::CircularReference { cycle } => {
write!(f, "circular reference: {}", cycle.join(" -> "))
}
Self::AbstractEntity(urn) => {
write!(f, "cannot instantiate abstract entity: {urn}")
}
Self::DuplicateUrn(urn) => write!(f, "duplicate entity URN: {urn}"),
Self::ResolutionError(msg) => write!(f, "resolution error: {msg}"),
}
}
}
impl std::error::Error for EntityResolverError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PropertyDiff {
OnlyInLeft(String),
OnlyInRight(String),
Modified {
name: String,
difference: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntityComparison {
pub left_urn: String,
pub right_urn: String,
pub is_equal: bool,
pub diffs: Vec<PropertyDiff>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlattenedEntity {
pub urn: String,
pub name: String,
pub properties: Vec<EntityProperty>,
pub hierarchy: Vec<String>,
pub is_abstract: bool,
}
pub struct EntityResolver {
entities: HashMap<String, EntityDefinition>,
}
impl EntityResolver {
pub fn new() -> Self {
Self {
entities: HashMap::new(),
}
}
pub fn register(&mut self, entity: EntityDefinition) -> Result<(), EntityResolverError> {
if self.entities.contains_key(&entity.urn) {
return Err(EntityResolverError::DuplicateUrn(entity.urn.clone()));
}
self.entities.insert(entity.urn.clone(), entity);
Ok(())
}
pub fn get(&self, urn: &str) -> Option<&EntityDefinition> {
self.entities.get(urn)
}
pub fn remove(&mut self, urn: &str) -> Option<EntityDefinition> {
self.entities.remove(urn)
}
pub fn list_urns(&self) -> Vec<&str> {
self.entities.keys().map(|k| k.as_str()).collect()
}
pub fn len(&self) -> usize {
self.entities.len()
}
pub fn is_empty(&self) -> bool {
self.entities.is_empty()
}
pub fn hierarchy(&self, urn: &str) -> Result<Vec<String>, EntityResolverError> {
let mut chain = Vec::new();
let mut visited = HashSet::new();
let mut current = urn.to_string();
loop {
if !visited.insert(current.clone()) {
chain.push(current);
return Err(EntityResolverError::CircularReference { cycle: chain });
}
let entity = self
.entities
.get(¤t)
.ok_or_else(|| EntityResolverError::NotFound(current.clone()))?;
chain.push(current.clone());
match &entity.extends {
Some(parent_urn) => {
current = parent_urn.clone();
}
None => break,
}
}
chain.reverse();
Ok(chain)
}
pub fn is_abstract(&self, urn: &str) -> Result<bool, EntityResolverError> {
let entity = self
.entities
.get(urn)
.ok_or_else(|| EntityResolverError::NotFound(urn.to_string()))?;
Ok(entity.is_abstract)
}
pub fn has_circular_reference(&self, urn: &str) -> bool {
self.hierarchy(urn).is_err()
}
pub fn flatten(&self, urn: &str) -> Result<FlattenedEntity, EntityResolverError> {
let hierarchy = self.hierarchy(urn)?;
let mut all_properties = Vec::new();
let mut seen_names = HashSet::new();
for ancestor_urn in &hierarchy {
let entity = self
.entities
.get(ancestor_urn.as_str())
.ok_or_else(|| EntityResolverError::NotFound(ancestor_urn.clone()))?;
for prop in &entity.properties {
if seen_names.insert(prop.name.clone()) {
all_properties.push(prop.clone());
}
}
}
let entity = self
.entities
.get(urn)
.ok_or_else(|| EntityResolverError::NotFound(urn.to_string()))?;
Ok(FlattenedEntity {
urn: urn.to_string(),
name: entity.name.clone(),
properties: all_properties,
hierarchy,
is_abstract: entity.is_abstract,
})
}
pub fn own_properties(&self, urn: &str) -> Result<Vec<EntityProperty>, EntityResolverError> {
let entity = self
.entities
.get(urn)
.ok_or_else(|| EntityResolverError::NotFound(urn.to_string()))?;
Ok(entity.properties.clone())
}
pub fn parent_urn(&self, urn: &str) -> Result<Option<String>, EntityResolverError> {
let entity = self
.entities
.get(urn)
.ok_or_else(|| EntityResolverError::NotFound(urn.to_string()))?;
Ok(entity.extends.clone())
}
pub fn children(&self, parent_urn: &str) -> Vec<&EntityDefinition> {
self.entities
.values()
.filter(|e| e.extends.as_deref() == Some(parent_urn))
.collect()
}
pub fn compare(
&self,
left_urn: &str,
right_urn: &str,
) -> Result<EntityComparison, EntityResolverError> {
let left = self.flatten(left_urn)?;
let right = self.flatten(right_urn)?;
let left_map: HashMap<&str, &EntityProperty> = left
.properties
.iter()
.map(|p| (p.name.as_str(), p))
.collect();
let right_map: HashMap<&str, &EntityProperty> = right
.properties
.iter()
.map(|p| (p.name.as_str(), p))
.collect();
let all_names: HashSet<&str> = left_map.keys().chain(right_map.keys()).copied().collect();
let mut diffs = Vec::new();
for name in &all_names {
match (left_map.get(name), right_map.get(name)) {
(Some(_), None) => {
diffs.push(PropertyDiff::OnlyInLeft(name.to_string()));
}
(None, Some(_)) => {
diffs.push(PropertyDiff::OnlyInRight(name.to_string()));
}
(Some(lp), Some(rp)) => {
if lp != rp {
let mut desc = Vec::new();
if lp.data_type != rp.data_type {
desc.push(format!("data_type: {} vs {}", lp.data_type, rp.data_type));
}
if lp.optional != rp.optional {
desc.push(format!("optional: {} vs {}", lp.optional, rp.optional));
}
if lp.description != rp.description {
desc.push("description differs".to_string());
}
diffs.push(PropertyDiff::Modified {
name: name.to_string(),
difference: desc.join("; "),
});
}
}
(None, None) => {} }
}
Ok(EntityComparison {
left_urn: left_urn.to_string(),
right_urn: right_urn.to_string(),
is_equal: diffs.is_empty(),
diffs,
})
}
}
impl Default for EntityResolver {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn vehicle_urn() -> String {
"urn:samm:org.example:1.0.0#Vehicle".to_string()
}
fn car_urn() -> String {
"urn:samm:org.example:1.0.0#Car".to_string()
}
fn ev_urn() -> String {
"urn:samm:org.example:1.0.0#ElectricVehicle".to_string()
}
fn vehicle_entity() -> EntityDefinition {
EntityDefinition::new(vehicle_urn(), "Vehicle")
.with_description("Base vehicle entity")
.as_abstract()
.with_property(EntityProperty::new(
"vin",
"xsd:string",
"Vehicle Identification Number",
))
.with_property(EntityProperty::new(
"manufacturer",
"xsd:string",
"Manufacturer name",
))
}
fn car_entity() -> EntityDefinition {
EntityDefinition::new(car_urn(), "Car")
.with_extends(vehicle_urn())
.with_property(EntityProperty::new(
"doors",
"xsd:integer",
"Number of doors",
))
}
fn ev_entity() -> EntityDefinition {
EntityDefinition::new(ev_urn(), "ElectricVehicle")
.with_extends(car_urn())
.with_property(EntityProperty::new(
"batteryCapacity",
"xsd:float",
"Battery capacity in kWh",
))
.with_property(EntityProperty::optional(
"range",
"xsd:float",
"Range in km",
))
}
fn build_resolver() -> EntityResolver {
let mut resolver = EntityResolver::new();
resolver
.register(vehicle_entity())
.expect("register vehicle");
resolver.register(car_entity()).expect("register car");
resolver.register(ev_entity()).expect("register ev");
resolver
}
#[test]
fn test_register_and_get() {
let mut resolver = EntityResolver::new();
resolver.register(vehicle_entity()).expect("register");
let entity = resolver.get(&vehicle_urn());
assert!(entity.is_some());
assert_eq!(entity.expect("exists").name, "Vehicle");
}
#[test]
fn test_register_duplicate() {
let mut resolver = EntityResolver::new();
resolver.register(vehicle_entity()).expect("first");
let result = resolver.register(vehicle_entity());
assert!(result.is_err());
match result {
Err(EntityResolverError::DuplicateUrn(u)) => assert_eq!(u, vehicle_urn()),
other => panic!("expected DuplicateUrn, got {other:?}"),
}
}
#[test]
fn test_get_nonexistent() {
let resolver = EntityResolver::new();
assert!(resolver.get("urn:nonexistent").is_none());
}
#[test]
fn test_remove() {
let mut resolver = EntityResolver::new();
resolver.register(vehicle_entity()).expect("register");
let removed = resolver.remove(&vehicle_urn());
assert!(removed.is_some());
assert!(resolver.get(&vehicle_urn()).is_none());
}
#[test]
fn test_list_urns() {
let resolver = build_resolver();
let urns = resolver.list_urns();
assert_eq!(urns.len(), 3);
}
#[test]
fn test_len_and_is_empty() {
let mut resolver = EntityResolver::new();
assert!(resolver.is_empty());
assert_eq!(resolver.len(), 0);
resolver.register(vehicle_entity()).expect("register");
assert!(!resolver.is_empty());
assert_eq!(resolver.len(), 1);
}
#[test]
fn test_default() {
let resolver = EntityResolver::default();
assert!(resolver.is_empty());
}
#[test]
fn test_hierarchy_root() {
let resolver = build_resolver();
let hierarchy = resolver.hierarchy(&vehicle_urn()).expect("hierarchy");
assert_eq!(hierarchy, vec![vehicle_urn()]);
}
#[test]
fn test_hierarchy_one_level() {
let resolver = build_resolver();
let hierarchy = resolver.hierarchy(&car_urn()).expect("hierarchy");
assert_eq!(hierarchy, vec![vehicle_urn(), car_urn()]);
}
#[test]
fn test_hierarchy_two_levels() {
let resolver = build_resolver();
let hierarchy = resolver.hierarchy(&ev_urn()).expect("hierarchy");
assert_eq!(hierarchy, vec![vehicle_urn(), car_urn(), ev_urn()]);
}
#[test]
fn test_hierarchy_not_found() {
let resolver = EntityResolver::new();
let result = resolver.hierarchy("urn:unknown");
assert!(result.is_err());
}
#[test]
fn test_circular_reference() {
let mut resolver = EntityResolver::new();
resolver
.register(EntityDefinition::new("urn:A", "A").with_extends("urn:B"))
.expect("register A");
resolver
.register(EntityDefinition::new("urn:B", "B").with_extends("urn:A"))
.expect("register B");
assert!(resolver.has_circular_reference("urn:A"));
let result = resolver.hierarchy("urn:A");
assert!(result.is_err());
match result {
Err(EntityResolverError::CircularReference { cycle }) => {
assert!(cycle.contains(&"urn:A".to_string()));
assert!(cycle.contains(&"urn:B".to_string()));
}
other => panic!("expected CircularReference, got {other:?}"),
}
}
#[test]
fn test_no_circular_reference() {
let resolver = build_resolver();
assert!(!resolver.has_circular_reference(&ev_urn()));
}
#[test]
fn test_is_abstract_true() {
let resolver = build_resolver();
assert!(resolver.is_abstract(&vehicle_urn()).expect("ok"));
}
#[test]
fn test_is_abstract_false() {
let resolver = build_resolver();
assert!(!resolver.is_abstract(&car_urn()).expect("ok"));
}
#[test]
fn test_is_abstract_not_found() {
let resolver = EntityResolver::new();
let result = resolver.is_abstract("urn:unknown");
assert!(result.is_err());
}
#[test]
fn test_flatten_root() {
let resolver = build_resolver();
let flat = resolver.flatten(&vehicle_urn()).expect("flatten");
assert_eq!(flat.properties.len(), 2);
assert_eq!(flat.hierarchy.len(), 1);
assert!(flat.is_abstract);
}
#[test]
fn test_flatten_one_level() {
let resolver = build_resolver();
let flat = resolver.flatten(&car_urn()).expect("flatten");
assert_eq!(flat.properties.len(), 3);
assert_eq!(flat.properties[0].name, "vin");
assert_eq!(flat.properties[1].name, "manufacturer");
assert_eq!(flat.properties[2].name, "doors");
}
#[test]
fn test_flatten_two_levels() {
let resolver = build_resolver();
let flat = resolver.flatten(&ev_urn()).expect("flatten");
assert_eq!(flat.properties.len(), 5);
let names: Vec<&str> = flat.properties.iter().map(|p| p.name.as_str()).collect();
assert!(names.contains(&"vin"));
assert!(names.contains(&"manufacturer"));
assert!(names.contains(&"doors"));
assert!(names.contains(&"batteryCapacity"));
assert!(names.contains(&"range"));
}
#[test]
fn test_flatten_deduplicates_properties() {
let mut resolver = EntityResolver::new();
resolver
.register(
EntityDefinition::new("urn:parent", "Parent").with_property(EntityProperty::new(
"name",
"xsd:string",
"Name",
)),
)
.expect("register parent");
resolver
.register(
EntityDefinition::new("urn:child", "Child")
.with_extends("urn:parent")
.with_property(EntityProperty::new("name", "xsd:string", "Overridden name")),
)
.expect("register child");
let flat = resolver.flatten("urn:child").expect("flatten");
assert_eq!(flat.properties.len(), 1);
}
#[test]
fn test_own_properties() {
let resolver = build_resolver();
let props = resolver.own_properties(&car_urn()).expect("own_properties");
assert_eq!(props.len(), 1);
assert_eq!(props[0].name, "doors");
}
#[test]
fn test_own_properties_not_found() {
let resolver = EntityResolver::new();
let result = resolver.own_properties("urn:unknown");
assert!(result.is_err());
}
#[test]
fn test_parent_urn_some() {
let resolver = build_resolver();
let parent = resolver.parent_urn(&car_urn()).expect("ok");
assert_eq!(parent, Some(vehicle_urn()));
}
#[test]
fn test_parent_urn_none() {
let resolver = build_resolver();
let parent = resolver.parent_urn(&vehicle_urn()).expect("ok");
assert!(parent.is_none());
}
#[test]
fn test_parent_urn_not_found() {
let resolver = EntityResolver::new();
assert!(resolver.parent_urn("urn:unknown").is_err());
}
#[test]
fn test_children() {
let resolver = build_resolver();
let children = resolver.children(&vehicle_urn());
assert_eq!(children.len(), 1);
assert_eq!(children[0].name, "Car");
}
#[test]
fn test_children_none() {
let resolver = build_resolver();
let children = resolver.children(&ev_urn());
assert!(children.is_empty());
}
#[test]
fn test_compare_same_entity() {
let resolver = build_resolver();
let cmp = resolver.compare(&car_urn(), &car_urn()).expect("compare");
assert!(cmp.is_equal);
assert!(cmp.diffs.is_empty());
}
#[test]
fn test_compare_different_entities() {
let resolver = build_resolver();
let cmp = resolver.compare(&car_urn(), &ev_urn()).expect("compare");
assert!(!cmp.is_equal);
assert!(!cmp.diffs.is_empty());
}
#[test]
fn test_compare_property_diff_types() {
let mut resolver = EntityResolver::new();
resolver
.register(
EntityDefinition::new("urn:a", "A").with_property(EntityProperty::new(
"x",
"xsd:string",
"X",
)),
)
.expect("register a");
resolver
.register(
EntityDefinition::new("urn:b", "B").with_property(EntityProperty::new(
"x",
"xsd:integer",
"X",
)),
)
.expect("register b");
let cmp = resolver.compare("urn:a", "urn:b").expect("compare");
assert!(!cmp.is_equal);
let modified = cmp
.diffs
.iter()
.find(|d| matches!(d, PropertyDiff::Modified { .. }));
assert!(modified.is_some());
}
#[test]
fn test_compare_only_in_left() {
let mut resolver = EntityResolver::new();
resolver
.register(
EntityDefinition::new("urn:a", "A")
.with_property(EntityProperty::new("x", "xsd:string", "X"))
.with_property(EntityProperty::new("y", "xsd:string", "Y")),
)
.expect("register a");
resolver
.register(EntityDefinition::new("urn:b", "B"))
.expect("register b");
let cmp = resolver.compare("urn:a", "urn:b").expect("compare");
let left_only: Vec<_> = cmp
.diffs
.iter()
.filter(|d| matches!(d, PropertyDiff::OnlyInLeft(_)))
.collect();
assert_eq!(left_only.len(), 2);
}
#[test]
fn test_compare_only_in_right() {
let mut resolver = EntityResolver::new();
resolver
.register(EntityDefinition::new("urn:a", "A"))
.expect("register a");
resolver
.register(
EntityDefinition::new("urn:b", "B").with_property(EntityProperty::new(
"z",
"xsd:float",
"Z",
)),
)
.expect("register b");
let cmp = resolver.compare("urn:a", "urn:b").expect("compare");
let right_only: Vec<_> = cmp
.diffs
.iter()
.filter(|d| matches!(d, PropertyDiff::OnlyInRight(_)))
.collect();
assert_eq!(right_only.len(), 1);
}
#[test]
fn test_entity_property_new() {
let p = EntityProperty::new("temp", "xsd:float", "Temperature");
assert_eq!(p.name, "temp");
assert_eq!(p.data_type, "xsd:float");
assert!(!p.optional);
}
#[test]
fn test_entity_property_optional() {
let p = EntityProperty::optional("tag", "xsd:string", "A tag");
assert!(p.optional);
}
#[test]
fn test_entity_definition_builder() {
let e = EntityDefinition::new("urn:test", "Test")
.with_description("A test entity")
.with_extends("urn:parent")
.as_abstract()
.with_property(EntityProperty::new("x", "xsd:int", "X"));
assert_eq!(e.name, "Test");
assert_eq!(e.description, Some("A test entity".into()));
assert_eq!(e.extends, Some("urn:parent".into()));
assert!(e.is_abstract);
assert_eq!(e.properties.len(), 1);
}
#[test]
fn test_error_display() {
let err = EntityResolverError::NotFound("urn:x".into());
assert!(err.to_string().contains("urn:x"));
let err2 = EntityResolverError::CircularReference {
cycle: vec!["urn:a".into(), "urn:b".into()],
};
assert!(err2.to_string().contains("urn:a -> urn:b"));
let err3 = EntityResolverError::AbstractEntity("urn:abs".into());
assert!(err3.to_string().contains("abstract"));
let err4 = EntityResolverError::DuplicateUrn("urn:dup".into());
assert!(err4.to_string().contains("duplicate"));
let err5 = EntityResolverError::ResolutionError("oops".into());
assert!(err5.to_string().contains("oops"));
}
#[test]
fn test_flattened_entity_hierarchy() {
let resolver = build_resolver();
let flat = resolver.flatten(&ev_urn()).expect("flatten");
assert_eq!(flat.hierarchy.len(), 3);
assert_eq!(flat.hierarchy[0], vehicle_urn());
assert_eq!(flat.hierarchy[2], ev_urn());
}
#[test]
fn test_flattened_entity_name() {
let resolver = build_resolver();
let flat = resolver.flatten(&ev_urn()).expect("flatten");
assert_eq!(flat.name, "ElectricVehicle");
}
#[test]
fn test_entity_with_no_properties() {
let mut resolver = EntityResolver::new();
resolver
.register(EntityDefinition::new("urn:empty", "Empty"))
.expect("register");
let flat = resolver.flatten("urn:empty").expect("flatten");
assert!(flat.properties.is_empty());
}
#[test]
fn test_remove_nonexistent() {
let mut resolver = EntityResolver::new();
assert!(resolver.remove("urn:nope").is_none());
}
#[test]
fn test_children_nonexistent_parent() {
let resolver = EntityResolver::new();
let children = resolver.children("urn:nope");
assert!(children.is_empty());
}
}