use std::collections::HashMap;
use crate::express::{Attribute, EntityDef, ParsedSchema, TypeDef, TypeKind};
const MAX_CHAIN_DEPTH: usize = 64;
#[derive(Debug, Clone)]
pub struct SchemaGraph {
name: String,
entities: HashMap<String, EntityDef>,
types: HashMap<String, TypeDef>,
children: HashMap<String, Vec<String>>,
}
impl SchemaGraph {
#[must_use]
pub fn new(parsed: ParsedSchema) -> Self {
let entities: HashMap<String, EntityDef> = parsed
.entities
.into_iter()
.map(|entity| (entity.name.to_ascii_uppercase(), entity))
.collect();
let mut children: HashMap<String, Vec<String>> = HashMap::new();
for entity in entities.values() {
if let Some(supertype) = &entity.supertype {
children
.entry(supertype.to_ascii_uppercase())
.or_default()
.push(entity.name.clone());
}
}
for names in children.values_mut() {
names.sort_unstable_by_key(|name| name.to_ascii_uppercase());
}
let types = parsed
.types
.into_iter()
.map(|type_def| (type_def.name.to_ascii_uppercase(), type_def))
.collect();
Self {
name: parsed.name,
entities,
types,
children,
}
}
#[must_use]
pub fn from_express(source: &str) -> Self {
Self::new(crate::express::parse(source))
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn entity_count(&self) -> usize {
self.entities.len()
}
#[must_use]
pub fn type_count(&self) -> usize {
self.types.len()
}
#[must_use]
pub fn entity(&self, name: &str) -> Option<&EntityDef> {
self.entities.get(&name.to_ascii_uppercase())
}
#[must_use]
pub fn type_def(&self, name: &str) -> Option<&TypeDef> {
self.types.get(&name.to_ascii_uppercase())
}
pub fn entity_names(&self) -> impl Iterator<Item = &str> {
self.entities.values().map(|entity| entity.name.as_str())
}
#[must_use]
pub fn is_a(&self, name: &str, ancestor: &str) -> bool {
if name.eq_ignore_ascii_case(ancestor) {
return self.entities.contains_key(&name.to_ascii_uppercase());
}
self.supertypes(name)
.iter()
.any(|super_name| super_name.eq_ignore_ascii_case(ancestor))
}
#[must_use]
pub fn supertypes(&self, name: &str) -> Vec<&str> {
let mut chain = Vec::new();
let mut current = self.entities.get(&name.to_ascii_uppercase());
for _ in 0..MAX_CHAIN_DEPTH {
let Some(def) = current else { break };
let Some(supertype) = def.supertype.as_ref() else {
break;
};
let Some(parent) = self.entities.get(&supertype.to_ascii_uppercase()) else {
chain.push(supertype.as_str());
break;
};
chain.push(parent.name.as_str());
current = Some(parent);
}
chain
}
#[must_use]
pub fn direct_subtypes(&self, name: &str) -> Vec<&str> {
self.children
.get(&name.to_ascii_uppercase())
.map(|names| names.iter().map(String::as_str).collect())
.unwrap_or_default()
}
#[must_use]
pub fn subtypes(&self, name: &str) -> Vec<&str> {
let mut seen = std::collections::HashSet::new();
seen.insert(name.to_ascii_uppercase());
let mut out = Vec::new();
let mut stack: Vec<&str> = self.direct_subtypes(name).into_iter().rev().collect();
while let Some(current) = stack.pop() {
if !seen.insert(current.to_ascii_uppercase()) {
continue;
}
out.push(current);
stack.extend(self.direct_subtypes(current).into_iter().rev());
}
out
}
#[must_use]
pub fn attributes(&self, name: &str) -> Vec<&Attribute> {
let mut chain: Vec<&EntityDef> = Vec::new();
let mut current = self.entities.get(&name.to_ascii_uppercase());
for _ in 0..MAX_CHAIN_DEPTH {
let Some(def) = current else { break };
chain.push(def);
let Some(supertype) = def.supertype.as_ref() else {
break;
};
current = self.entities.get(&supertype.to_ascii_uppercase());
}
chain.reverse();
chain.iter().flat_map(|def| def.attributes.iter()).collect()
}
#[must_use]
pub fn attribute_names(&self, name: &str) -> Vec<&str> {
self.attributes(name)
.into_iter()
.map(|attribute| attribute.name.as_str())
.collect()
}
#[must_use]
pub fn resolve_defined(&self, name: &str) -> String {
let mut current = name.to_string();
for _ in 0..MAX_CHAIN_DEPTH {
let Some(def) = self.type_def(¤t) else {
return current;
};
let TypeKind::Defined(target) = &def.kind else {
return current;
};
let next = target.trim().to_string();
if next.eq_ignore_ascii_case(¤t) {
return current;
}
current = next;
}
current
}
}
#[cfg(test)]
mod tests {
use super::*;
const CHAIN: &str = "\
SCHEMA DEMO;
ENTITY Base
ABSTRACT SUPERTYPE OF (ONEOF(Middle));
Id : Identifier;
Owner : OPTIONAL Party;
Name : OPTIONAL Label;
Description : OPTIONAL Text;
END_ENTITY;
ENTITY Middle
ABSTRACT SUPERTYPE OF (ONEOF(Leaf))
SUBTYPE OF (Base);
END_ENTITY;
ENTITY Leaf
SUBTYPE OF (Middle);
Kind : OPTIONAL Label;
END_ENTITY;
TYPE Count = INTEGER; END_TYPE;
TYPE PositiveCount = Count; END_TYPE;
TYPE Colour = ENUMERATION OF (RED, GREEN, NOTDEFINED); END_TYPE;
END_SCHEMA;";
fn graph() -> SchemaGraph {
SchemaGraph::from_express(CHAIN)
}
#[test]
fn inherited_attributes_come_first_in_positional_order() {
assert_eq!(
graph().attribute_names("LEAF"),
["Id", "Owner", "Name", "Description", "Kind"],
"Base's slots must precede Leaf's own"
);
}
#[test]
fn subtype_tests_cross_intermediate_levels() {
let schema = graph();
assert!(schema.is_a("LEAF", "Base"), "grandparent");
assert!(schema.is_a("Leaf", "Middle"), "parent");
assert!(schema.is_a("Leaf", "Leaf"), "reflexive");
assert!(!schema.is_a("Base", "Leaf"), "not upward");
}
#[test]
fn an_undeclared_entity_is_not_a_subtype_even_of_itself() {
assert!(!graph().is_a("NotAThing", "NotAThing"));
}
#[test]
fn defined_types_resolve_through_the_alias_chain() {
assert_eq!(graph().resolve_defined("PositiveCount"), "INTEGER");
}
#[test]
fn an_enumeration_resolves_to_its_own_name() {
assert_eq!(graph().resolve_defined("Colour"), "Colour");
}
#[test]
fn a_cyclic_supertype_chain_terminates() {
let schema = SchemaGraph::from_express(
"SCHEMA S;\
ENTITY A SUBTYPE OF (B); END_ENTITY;\
ENTITY B SUBTYPE OF (A); END_ENTITY;\
END_SCHEMA;",
);
assert!(schema.supertypes("A").len() <= MAX_CHAIN_DEPTH);
}
#[test]
fn a_cyclic_alias_chain_terminates() {
let schema = SchemaGraph::from_express(
"SCHEMA S; TYPE A = B; END_TYPE; TYPE B = A; END_TYPE; END_SCHEMA;",
);
let resolved = schema.resolve_defined("A");
assert!(resolved == "A" || resolved == "B");
}
#[test]
fn an_undeclared_supertype_is_still_named() {
let schema = SchemaGraph::from_express(
"SCHEMA S; ENTITY A SUBTYPE OF (Missing); END_ENTITY; END_SCHEMA;",
);
assert_eq!(schema.supertypes("A"), ["Missing"]);
assert!(schema.is_a("A", "Missing"));
assert_eq!(
schema.subtypes("Missing"),
["A"],
"the downward walk must agree with is_a on a partial schema"
);
}
const TREE: &str = "\
SCHEMA TREE;
ENTITY Root; END_ENTITY;
ENTITY Wall SUBTYPE OF (Root); END_ENTITY;
ENTITY Door SUBTYPE OF (Root); END_ENTITY;
ENTITY WallStandardCase SUBTYPE OF (Wall); END_ENTITY;
ENTITY WallElementedCase SUBTYPE OF (Wall); END_ENTITY;
ENTITY Unrelated; END_ENTITY;
END_SCHEMA;";
#[test]
fn direct_subtypes_are_one_level_and_sorted() {
let schema = SchemaGraph::from_express(TREE);
assert_eq!(schema.direct_subtypes("root"), ["Door", "Wall"]);
assert!(schema.direct_subtypes("Door").is_empty(), "a leaf");
assert!(schema.direct_subtypes("NotAThing").is_empty());
}
#[test]
fn subtypes_walk_every_depth_in_sorted_pre_order() {
let schema = SchemaGraph::from_express(TREE);
assert_eq!(
schema.subtypes("ROOT"),
["Door", "Wall", "WallElementedCase", "WallStandardCase"],
"a grandchild must follow its own parent, not trail the whole level"
);
assert!(!schema.subtypes("Root").contains(&"Root"), "not reflexive");
assert!(!schema.subtypes("Root").contains(&"Unrelated"));
}
#[test]
fn subtypes_is_exactly_the_inverse_of_is_a() {
for source in [CHAIN, TREE] {
let schema = SchemaGraph::from_express(source);
let names: Vec<&str> = schema.entity_names().collect();
for &ancestor in &names {
let down = schema.subtypes(ancestor);
for &candidate in &names {
let expected = candidate != ancestor && schema.is_a(candidate, ancestor);
assert_eq!(
down.contains(&candidate),
expected,
"{candidate} vs {ancestor}"
);
}
}
}
}
#[test]
fn a_cyclic_subtype_walk_terminates() {
let schema = SchemaGraph::from_express(
"SCHEMA S;\
ENTITY A SUBTYPE OF (B); END_ENTITY;\
ENTITY B SUBTYPE OF (A); END_ENTITY;\
END_SCHEMA;",
);
assert_eq!(schema.subtypes("A"), ["B"]);
}
}