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>,
}
impl SchemaGraph {
#[must_use]
pub fn new(parsed: ParsedSchema) -> Self {
let entities = parsed
.entities
.into_iter()
.map(|entity| (entity.name.to_ascii_uppercase(), entity))
.collect();
let types = parsed
.types
.into_iter()
.map(|type_def| (type_def.name.to_ascii_uppercase(), type_def))
.collect();
Self {
name: parsed.name,
entities,
types,
}
}
#[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 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"));
}
}