use std::collections::HashSet;
use openbim_step::express::{Attribute, EntityDef, ParsedSchema, TypeDef, TypeKind};
use openbim_step::SchemaGraph;
use crate::version::SchemaVersion;
#[derive(Debug, Clone)]
pub struct Schema {
graph: SchemaGraph,
}
impl Schema {
#[must_use]
pub fn from_parsed(parsed: ParsedSchema) -> Self {
Self {
graph: SchemaGraph::new(parsed),
}
}
#[must_use]
pub fn from_express(source: &str) -> Self {
Self {
graph: SchemaGraph::from_express(source),
}
}
#[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.graph.name()
}
#[must_use]
pub fn version(&self) -> Option<SchemaVersion> {
SchemaVersion::from_header_token(self.graph.name())
}
#[must_use]
pub fn entity_count(&self) -> usize {
self.graph.entity_count()
}
#[must_use]
pub fn type_count(&self) -> usize {
self.graph.type_count()
}
#[must_use]
pub fn entity(&self, name: &str) -> Option<&EntityDef> {
self.graph.entity(name)
}
#[must_use]
pub fn type_def(&self, name: &str) -> Option<&TypeDef> {
self.graph.type_def(name)
}
pub fn entity_names(&self) -> impl Iterator<Item = &str> {
self.graph.entity_names()
}
#[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 {
self.graph.is_a(name, ancestor)
}
#[must_use]
pub fn supertypes(&self, name: &str) -> Vec<&str> {
self.graph.supertypes(name)
}
#[must_use]
pub fn direct_subtypes(&self, name: &str) -> Vec<&str> {
self.graph.direct_subtypes(name)
}
#[must_use]
pub fn subtypes(&self, name: &str) -> Vec<&str> {
self.graph.subtypes(name)
}
#[must_use]
pub fn attributes(&self, name: &str) -> Vec<&Attribute> {
self.graph.attributes(name)
}
#[must_use]
pub fn attribute_names(&self, name: &str) -> Vec<&str> {
self.graph.attribute_names(name)
}
#[must_use]
pub fn resolve_defined(&self, name: &str) -> String {
self.graph.resolve_defined(name)
}
#[must_use]
pub fn graph(&self) -> &SchemaGraph {
&self.graph
}
}
#[cfg(test)]
mod tests {
use super::*;
const CHAIN: &str = "\
SCHEMA IFC4;
ENTITY IfcRoot
ABSTRACT SUPERTYPE OF (ONEOF(IfcObjectDefinition));
GlobalId : IfcGloballyUniqueId;
OwnerHistory : OPTIONAL IfcOwnerHistory;
Name : OPTIONAL IfcLabel;
Description : OPTIONAL IfcText;
END_ENTITY;
ENTITY IfcObjectDefinition
ABSTRACT SUPERTYPE OF (ONEOF(IfcObject))
SUBTYPE OF (IfcRoot);
END_ENTITY;
ENTITY IfcObject
SUBTYPE OF (IfcObjectDefinition);
ObjectType : OPTIONAL IfcLabel;
END_ENTITY;
TYPE IfcLengthMeasure = REAL; END_TYPE;
TYPE IfcPositiveLengthMeasure = IfcLengthMeasure; END_TYPE;
END_SCHEMA;";
#[test]
fn the_declared_schema_name_maps_onto_a_known_ifc_version() {
let schema = Schema::from_express(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::from_express(
"SCHEMA AP242; ENTITY Product; Id : Identifier; END_ENTITY; END_SCHEMA;",
);
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!(
Schema::from_express(CHAIN).attribute_names("IFCOBJECT"),
[
"GlobalId",
"OwnerHistory",
"Name",
"Description",
"ObjectType"
],
);
}
#[test]
fn defined_types_resolve_through_the_alias_chain() {
assert_eq!(
Schema::from_express(CHAIN).resolve_defined("IfcPositiveLengthMeasure"),
"REAL"
);
}
}