use std::collections::{HashMap, HashSet};
use crate::attribute::Attribute;
use crate::entity::EntityDef;
use crate::types::{TypeDef, TypeKind};
use crate::version::SchemaVersion;
const MAX_CHAIN_DEPTH: usize = 64;
#[derive(Debug, Clone)]
pub struct Schema {
name: String,
entities: Vec<EntityDef>,
types: Vec<TypeDef>,
entity_index: HashMap<String, usize>,
type_index: HashMap<String, usize>,
children: HashMap<String, Vec<String>>,
}
impl PartialEq for Schema {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.entities == other.entities && self.types == other.types
}
}
impl Eq for Schema {}
impl Schema {
#[must_use]
pub fn new(name: impl Into<String>, entities: Vec<EntityDef>, types: Vec<TypeDef>) -> Self {
let (entities, entity_index) = index_by_name(entities, |entity| &entity.name);
let (types, type_index) = index_by_name(types, |type_def| &type_def.name);
let mut children: HashMap<String, Vec<String>> = HashMap::new();
for entity in &entities {
for supertype in &entity.supertypes {
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());
names.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
}
Self {
name: name.into(),
entities,
types,
entity_index,
type_index,
children,
}
}
#[cfg(feature = "express")]
#[must_use]
pub fn from_express(source: &str) -> Self {
crate::express::parse(source)
}
#[cfg(feature = "express")]
#[must_use]
pub fn from_express_bytes(bytes: &[u8]) -> Self {
let text: String = bytes.iter().map(|&byte| byte as char).collect();
Self::from_express(&text)
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn version(&self) -> Option<SchemaVersion> {
SchemaVersion::from_header_token(&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.entity_index
.get(&name.to_ascii_uppercase())
.map(|&index| &self.entities[index])
}
#[must_use]
pub fn type_def(&self, name: &str) -> Option<&TypeDef> {
self.type_index
.get(&name.to_ascii_uppercase())
.map(|&index| &self.types[index])
}
pub fn entities(&self) -> impl Iterator<Item = &EntityDef> {
self.entities.iter()
}
pub fn types(&self) -> impl Iterator<Item = &TypeDef> {
self.types.iter()
}
pub fn entity_names(&self) -> impl Iterator<Item = &str> {
self.entities.iter().map(|entity| entity.name.as_str())
}
#[must_use]
pub fn accepts_type(&self, declared: &str, candidate: &str) -> bool {
self.accepts_type_inner(declared, candidate, &mut HashSet::new(), 32)
}
fn accepts_type_inner(
&self,
declared: &str,
candidate: &str,
seen: &mut HashSet<(String, String)>,
depth: usize,
) -> bool {
if declared.eq_ignore_ascii_case(candidate) {
return self.entity(declared).is_some() || self.type_def(declared).is_some();
}
if depth == 0
|| !seen.insert((
declared.to_ascii_uppercase(),
candidate.to_ascii_uppercase(),
))
{
return false;
}
if self.entity(declared).is_some() && self.entity(candidate).is_some() {
return self.is_a(candidate, declared);
}
if let Some(definition) = self.type_def(declared) {
match &definition.kind {
TypeKind::Defined(alias) => {
if self.accepts_type_inner(alias, candidate, seen, depth - 1) {
return true;
}
}
TypeKind::Select(members) => {
if members
.iter()
.any(|member| self.accepts_type_inner(member, candidate, seen, depth - 1))
{
return true;
}
}
TypeKind::Enumeration(_) => {}
}
}
if let Some(definition) = self.type_def(candidate) {
if let TypeKind::Defined(alias) = &definition.kind {
return self.accepts_type_inner(declared, alias, seen, depth - 1);
}
}
false
}
#[must_use]
pub fn is_a(&self, name: &str, ancestor: &str) -> bool {
if name.eq_ignore_ascii_case(ancestor) {
return self.entity(name).is_some();
}
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 seen = HashSet::new();
seen.insert(name.to_ascii_uppercase());
let mut out = Vec::new();
self.collect_supertypes(name, 0, &mut seen, &mut out);
out
}
fn collect_supertypes<'s>(
&'s self,
name: &str,
depth: usize,
seen: &mut HashSet<String>,
out: &mut Vec<&'s str>,
) {
if depth >= MAX_CHAIN_DEPTH {
return;
}
let Some(def) = self.entity(name) else {
return;
};
for supertype in &def.supertypes {
if !seen.insert(supertype.to_ascii_uppercase()) {
continue;
}
match self.entity(supertype) {
Some(parent) => {
out.push(parent.name.as_str());
self.collect_supertypes(&parent.name, depth + 1, seen, out);
}
None => out.push(supertype.as_str()),
}
}
}
#[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 = 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 seen = HashSet::new();
let mut out = Vec::new();
self.collect_attributes(name, 0, &mut seen, &mut out);
out
}
fn collect_attributes<'s>(
&'s self,
name: &str,
depth: usize,
seen: &mut HashSet<String>,
out: &mut Vec<&'s Attribute>,
) {
if depth > MAX_CHAIN_DEPTH {
return;
}
let Some(def) = self.entity(name) else {
return;
};
if !seen.insert(def.name.to_ascii_uppercase()) {
return;
}
for supertype in &def.supertypes {
self.collect_attributes(supertype, depth + 1, seen, out);
}
out.extend(def.attributes.iter());
}
#[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
}
}
fn index_by_name<T>(
declarations: Vec<T>,
name: impl Fn(&T) -> &String,
) -> (Vec<T>, HashMap<String, usize>) {
let mut kept: Vec<T> = Vec::with_capacity(declarations.len());
let mut index = HashMap::with_capacity(declarations.len());
for declaration in declarations {
let key = name(&declaration).to_ascii_uppercase();
match index.get(&key) {
Some(&position) => kept[position] = declaration,
None => {
index.insert(key, kept.len());
kept.push(declaration);
}
}
}
(kept, index)
}
#[cfg(test)]
mod tests {
use super::*;
fn chain() -> Schema {
Schema::new(
"IFC4",
vec![
EntityDef::new("IfcRoot")
.abstract_entity()
.with_attribute(Attribute::new("GlobalId", "IfcGloballyUniqueId"))
.with_attribute(Attribute::new("OwnerHistory", "IfcOwnerHistory").optional())
.with_attribute(Attribute::new("Name", "IfcLabel").optional())
.with_attribute(Attribute::new("Description", "IfcText").optional()),
EntityDef::new("IfcObjectDefinition")
.abstract_entity()
.with_supertype("IfcRoot"),
EntityDef::new("IfcObject")
.with_supertype("IfcObjectDefinition")
.with_attribute(Attribute::new("ObjectType", "IfcLabel").optional()),
],
vec![
TypeDef::new("IfcLengthMeasure", TypeKind::Defined("REAL".into())),
TypeDef::new(
"IfcPositiveLengthMeasure",
TypeKind::Defined("IfcLengthMeasure".into()),
),
],
)
}
#[test]
fn the_declared_schema_name_maps_onto_a_known_ifc_version() {
let schema = chain();
assert_eq!(schema.name(), "IFC4");
assert_eq!(schema.version(), Some(SchemaVersion::Ifc4));
}
#[test]
fn an_unrecognized_schema_name_has_no_version_but_still_works() {
let schema = Schema::new(
"AP242",
vec![EntityDef::new("Product").with_attribute(Attribute::new("Id", "Identifier"))],
Vec::new(),
);
assert_eq!(schema.version(), None, "not an IFC schema");
assert_eq!(schema.attribute_names("Product"), ["Id"]);
}
#[test]
fn inherited_attributes_come_first_in_positional_order() {
assert_eq!(
chain().attribute_names("IFCOBJECT"),
[
"GlobalId",
"OwnerHistory",
"Name",
"Description",
"ObjectType"
],
);
}
#[test]
fn defined_types_resolve_through_the_alias_chain() {
assert_eq!(chain().resolve_defined("IfcPositiveLengthMeasure"), "REAL");
}
#[test]
fn subtype_tests_cross_intermediate_levels_and_are_reflexive_only_when_declared() {
let schema = chain();
assert!(schema.is_a("IFCOBJECT", "IfcRoot"));
assert!(schema.is_a("IfcObject", "IfcObject"));
assert!(!schema.is_a("IfcRoot", "IfcObject"));
assert!(!schema.is_a("NotAThing", "NotAThing"));
assert_eq!(
schema.subtypes("ifcroot"),
["IfcObjectDefinition", "IfcObject"]
);
assert_eq!(
schema.supertypes("IfcObject"),
["IfcObjectDefinition", "IfcRoot"]
);
}
#[test]
fn cyclic_chains_terminate() {
let schema = Schema::new(
"S",
vec![
EntityDef::new("A").with_supertype("B"),
EntityDef::new("B").with_supertype("A"),
],
vec![
TypeDef::new("X", TypeKind::Defined("Y".into())),
TypeDef::new("Y", TypeKind::Defined("X".into())),
],
);
assert_eq!(schema.supertypes("A"), ["B"]);
assert_eq!(schema.subtypes("A"), ["B"]);
let resolved = schema.resolve_defined("X");
assert!(resolved == "X" || resolved == "Y");
}
#[test]
fn an_undeclared_supertype_is_still_named_in_both_directions() {
let schema = Schema::new(
"S",
vec![EntityDef::new("A").with_supertype("Missing")],
Vec::new(),
);
assert_eq!(schema.supertypes("A"), ["Missing"]);
assert!(schema.is_a("A", "Missing"));
assert_eq!(schema.subtypes("Missing"), ["A"]);
}
#[test]
fn a_repeated_name_keeps_its_first_position_and_last_declaration() {
let schema = Schema::new(
"S",
vec![
EntityDef::new("A"),
EntityDef::new("B"),
EntityDef::new("a").with_attribute(Attribute::new("X", "INTEGER")),
],
Vec::new(),
);
assert_eq!(schema.entity_count(), 2);
assert_eq!(schema.entity_names().collect::<Vec<_>>(), ["a", "B"]);
assert_eq!(schema.attribute_names("A"), ["X"]);
}
}
#[cfg(all(test, feature = "express"))]
mod parity_with_step {
use super::*;
const TREE: &str = "\
SCHEMA TREE;
ENTITY Root; Id : INTEGER; END_ENTITY;
ENTITY Wall SUBTYPE OF (Root); Name : OPTIONAL STRING; END_ENTITY;
ENTITY Door SUBTYPE OF (Root); END_ENTITY;
ENTITY WallStandardCase SUBTYPE OF (Wall); Tag : LIST [1:?] OF STRING; END_ENTITY;
ENTITY WallElementedCase SUBTYPE OF (Wall); END_ENTITY;
ENTITY Orphan SUBTYPE OF (Missing); END_ENTITY;
ENTITY Unrelated; END_ENTITY;
TYPE Count = INTEGER; END_TYPE;
TYPE PositiveCount = Count; END_TYPE;
TYPE Colour = ENUMERATION OF (RED, GREEN); END_TYPE;
END_SCHEMA;";
#[test]
fn every_query_matches_the_step_schema_graph() {
let owned = Schema::from_express(TREE);
let graph = openbim_step::SchemaGraph::from_express(TREE);
assert_eq!(owned.name(), graph.name());
assert_eq!(owned.entity_count(), graph.entity_count());
assert_eq!(owned.type_count(), graph.type_count());
let mut names: Vec<&str> = graph.entity_names().collect();
names.extend(["Missing", "NotAThing"]);
for &name in &names {
assert_eq!(owned.supertypes(name), graph.supertypes(name), "{name}");
assert_eq!(owned.subtypes(name), graph.subtypes(name), "{name}");
assert_eq!(
owned.direct_subtypes(name),
graph.direct_subtypes(name),
"{name}"
);
assert_eq!(
owned.attribute_names(name),
graph.attribute_names(name),
"{name}"
);
for &ancestor in &names {
assert_eq!(
owned.is_a(name, ancestor),
graph.is_a(name, ancestor),
"{name} / {ancestor}"
);
}
}
for name in ["PositiveCount", "Colour", "Count", "Unknown"] {
assert_eq!(owned.resolve_defined(name), graph.resolve_defined(name));
}
}
}