use ifc_model::Model;
use ifc_schema::{for_version, Schema, SchemaVersion};
use crate::error::SchemaResolutionError;
#[derive(Clone, Copy)]
pub(crate) struct Release {
pub(crate) version: SchemaVersion,
pub(crate) schema: &'static Schema,
}
impl Release {
pub(crate) fn is_a(self, candidate: &str, ancestor: &str) -> bool {
self.schema.is_a(candidate, ancestor)
}
pub(crate) fn slot(self, entity: &str, attribute: &str) -> Option<usize> {
self.schema
.attribute_names(entity)
.iter()
.position(|name| name.eq_ignore_ascii_case(attribute))
}
fn bind(version: SchemaVersion) -> Result<Self, SchemaResolutionError> {
let schema =
for_version(version).map_err(|_| SchemaResolutionError::UnsupportedSchema {
schema: version.release_id().to_owned(),
})?;
Ok(Self { version, schema })
}
}
const VERIFIED: &[SchemaVersion] = &[
SchemaVersion::Ifc2x3,
SchemaVersion::Ifc4,
SchemaVersion::Ifc4x3,
];
pub fn schema_of(model: &Model) -> Result<SchemaVersion, SchemaResolutionError> {
resolve(model).map(|release| release.version)
}
pub(crate) fn resolve(model: &Model) -> Result<Release, SchemaResolutionError> {
match model.header().schema.as_slice() {
[] => Err(SchemaResolutionError::MissingSchema),
[token] => match SchemaVersion::from_header_token(token) {
Some(version) if VERIFIED.contains(&version) => Release::bind(version),
_ => Err(SchemaResolutionError::UnsupportedSchema {
schema: token.clone(),
}),
},
schemas => Err(SchemaResolutionError::MultipleSchemas {
schemas: schemas.len(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slots_hold_in_every_verified_release() {
use crate::connectivity::relation::slot as connects;
use crate::port::definition::slot as port;
use crate::system::group::slot as group;
use crate::zone::spatial_group::slot as spatial;
let pinned = [
(
"IFCRELASSIGNSTOGROUP",
"RelatedObjects",
group::ASSIGNS_RELATED,
),
(
"IFCRELASSIGNSTOGROUP",
"RelatingGroup",
group::ASSIGNS_GROUP,
),
("IFCRELCONNECTSPORTS", "RelatingPort", connects::RELATING),
("IFCRELCONNECTSPORTS", "RelatedPort", connects::RELATED),
(
"IFCRELCONNECTSPORTS",
"RealizingElement",
connects::REALIZING,
),
("IFCRELNESTS", "RelatingObject", port::NESTS_PARENT),
("IFCRELNESTS", "RelatedObjects", port::NESTS_CHILDREN),
(
"IFCRELCONNECTSPORTTOELEMENT",
"RelatingPort",
port::PORT_TO_ELEMENT_PORT,
),
(
"IFCRELCONNECTSPORTTOELEMENT",
"RelatedElement",
port::PORT_TO_ELEMENT_ELEMENT,
),
("IFCDISTRIBUTIONPORT", "FlowDirection", port::FLOW_DIRECTION),
(
"IFCRELCONTAINEDINSPATIALSTRUCTURE",
"RelatedElements",
spatial::RELATED_ELEMENTS,
),
(
"IFCRELCONTAINEDINSPATIALSTRUCTURE",
"RelatingStructure",
spatial::RELATING_STRUCTURE,
),
(
"IFCRELREFERENCEDINSPATIALSTRUCTURE",
"RelatedElements",
spatial::RELATED_ELEMENTS,
),
(
"IFCRELREFERENCEDINSPATIALSTRUCTURE",
"RelatingStructure",
spatial::RELATING_STRUCTURE,
),
("IFCSYSTEM", "Name", group::NAME),
("IFCDISTRIBUTIONPORT", "Name", port::NAME),
];
let mut checked = 0;
for &version in VERIFIED {
let release = Release::bind(version).expect("verified releases are bundled");
for (entity, attribute, slot) in pinned {
assert_eq!(
release.slot(entity, attribute),
Some(slot),
"{version:?} {entity}.{attribute}"
);
checked += 1;
}
}
assert_eq!(checked, 3 * pinned.len());
}
#[test]
fn unbound_and_unverified_headers_are_refused() {
let mut model = Model::new();
assert!(matches!(
resolve(&model),
Err(SchemaResolutionError::MissingSchema)
));
for token in ["IFC4X1", "IFC4X2", "IFC9"] {
model.header_mut().schema = vec![token.to_owned()];
assert!(
matches!(resolve(&model), Err(SchemaResolutionError::UnsupportedSchema { ref schema }) if schema == token),
"{token}"
);
}
model.header_mut().schema = vec!["IFC4".into(), "IFC2X3".into()];
assert!(matches!(
resolve(&model),
Err(SchemaResolutionError::MultipleSchemas { schemas: 2 })
));
model.header_mut().schema = vec!["IFC4X3_ADD2".into()];
assert_eq!(schema_of(&model), Ok(SchemaVersion::Ifc4x3));
}
}