#[cfg(feature = "artifact")]
use std::sync::OnceLock;
#[cfg(feature = "artifact")]
use crate::artifact::decode_schema;
use crate::registry::Schema;
use crate::version::SchemaVersion;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct NotBundled {
pub version: SchemaVersion,
}
impl std::fmt::Display for NotBundled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"IFC release {} is not compiled into this build (enable the `{}` feature of ifc-schema)",
self.version.release_id(),
self.version.feature_name()
)
}
}
impl std::error::Error for NotBundled {}
macro_rules! bundled_release {
($feature:literal, $cell:ident, $name:ident, $file:literal, $label:literal, $doc:literal) => {
#[cfg(feature = $feature)]
static $cell: OnceLock<Schema> = OnceLock::new();
#[doc = $doc]
#[doc = concat!("Requires the `", $feature, "` feature (on by default).")]
#[cfg(feature = $feature)]
#[must_use]
pub fn $name() -> &'static Schema {
$cell.get_or_init(|| {
decode_schema(include_bytes!($file)).expect(concat!(
"the bundled ",
$label,
" artifact is produced and verified by this crate's own build"
))
})
}
};
}
bundled_release!(
"ifc2x3",
IFC2X3,
ifc2x3,
"../data/ifc2x3-tc1.bin",
"IFC2x3",
"The bundled IFC2x3 TC1 schema (653 entities, 327 types).\n\n\
Still the most common schema in the wild. Its layouts differ from IFC4 \
in ways that silently corrupt a reader that assumes the newer tables: \
`IfcWallStandardCase` has 8 attributes here and 9 in IFC4, because IFC4 \
inserts `PredefinedType`."
);
bundled_release!(
"ifc4",
IFC4,
ifc4,
"../data/ifc4-add2-tc1.bin",
"IFC4",
"The bundled IFC4 ADD2 TC1 schema (776 entities, 397 types).\n\n\
Building it costs a `bincode` decode of a committed artifact: the \
`IFC4.exp` EXPRESS source is never read at runtime and is not in the \
published crate. Custom schema files remain available through \
`Schema::from_express` (the `express` feature) or [`Schema::new`]."
);
bundled_release!(
"ifc4x1",
IFC4X1,
ifc4x1,
"../data/ifc4x1-final.bin",
"IFC4X1",
"The bundled IFC4X1 FINAL schema (801 entities, 400 types).\n\n\
Its own artifact: IFC4X1 adds the alignment entities to IFC4 and is not \
an alias for either IFC4 or IFC4X3."
);
bundled_release!(
"ifc4x2",
IFC4X2,
ifc4x2,
"../data/ifc4x2-final.bin",
"IFC4X2",
"The bundled IFC4X2 FINAL schema (816 entities, 407 types).\n\n\
Its own artifact: IFC4X2 adds bridges to IFC4X1 and is not an alias for \
IFC4 or IFC4X3."
);
bundled_release!(
"ifc4x3",
IFC4X3,
ifc4x3,
"../data/ifc4x3-add2.bin",
"IFC4X3",
"The bundled IFC4X3 ADD2 schema (876 entities, 436 types).\n\n\
Its own artifact. It is never an alias for IFC4: renamed and civil \
entities require the declared tables."
);
pub fn for_version(version: SchemaVersion) -> Result<&'static Schema, NotBundled> {
match version {
#[cfg(feature = "ifc2x3")]
SchemaVersion::Ifc2x3 => Ok(ifc2x3()),
#[cfg(feature = "ifc4")]
SchemaVersion::Ifc4 => Ok(ifc4()),
#[cfg(feature = "ifc4x1")]
SchemaVersion::Ifc4x1 => Ok(ifc4x1()),
#[cfg(feature = "ifc4x2")]
SchemaVersion::Ifc4x2 => Ok(ifc4x2()),
#[cfg(feature = "ifc4x3")]
SchemaVersion::Ifc4x3 => Ok(ifc4x3()),
#[allow(unreachable_patterns)]
_ => Err(NotBundled { version }),
}
}
#[cfg(all(
test,
feature = "ifc2x3",
feature = "ifc4",
feature = "ifc4x1",
feature = "ifc4x2",
feature = "ifc4x3"
))]
mod tests {
use super::*;
#[test]
fn bundled_ifc4_matches_the_normative_entity_and_type_counts() {
let schema = ifc4();
assert_eq!(schema.entity_count(), 776, "IFC4 ADD2 TC1 entity count");
assert_eq!(schema.type_count(), 397, "IFC4 ADD2 TC1 type count");
}
#[test]
fn bundled_ifc4x3_matches_the_normative_entity_and_type_counts() {
let schema = ifc4x3();
assert_eq!(schema.entity_count(), 876, "IFC4X3 ADD2 entity count");
assert_eq!(schema.type_count(), 436, "IFC4X3 ADD2 type count");
assert!(schema.entity("IfcBuiltElement").is_some());
assert!(schema.entity("IfcBuildingElement").is_none());
assert!(std::ptr::eq(schema, ifc4x3()), "constructor must cache");
}
#[test]
fn bundled_ifc2x3_matches_the_normative_entity_and_type_counts() {
let schema = ifc2x3();
assert_eq!(schema.entity_count(), 653, "IFC2x3 TC1 entity count");
assert_eq!(schema.type_count(), 327, "IFC2x3 TC1 type count");
}
#[test]
fn bundled_ifc4_resolves_the_deep_inheritance_chain() {
let schema = ifc4();
assert!(schema.is_a("IFCWALL", "IfcRoot"), "wall is a root");
assert!(schema.is_a("IFCWALL", "IfcProduct"), "wall is a product");
assert_eq!(
&schema.attribute_names("IFCWALL")[..4],
["GlobalId", "OwnerHistory", "Name", "Description"],
"IfcRoot's slots must come first"
);
}
#[test]
fn version_lookup_returns_the_matching_table() {
assert_eq!(
for_version(SchemaVersion::Ifc4).map(|s| s.entity_count()),
Ok(776)
);
assert_eq!(
for_version(SchemaVersion::Ifc2x3).map(|s| s.entity_count()),
Ok(653)
);
assert_eq!(
for_version(SchemaVersion::Ifc4x3).map(|s| s.entity_count()),
Ok(876),
"IFC4X3 must select its own bundled tables"
);
}
#[test]
fn the_ifc2x3_and_ifc4_bundles_are_not_the_same_table() {
assert_eq!(
ifc2x3().attribute_names("IFCWALLSTANDARDCASE"),
[
"GlobalId",
"OwnerHistory",
"Name",
"Description",
"ObjectType",
"ObjectPlacement",
"Representation",
"Tag"
],
);
assert_eq!(
ifc4().attribute_names("IFCWALLSTANDARDCASE"),
[
"GlobalId",
"OwnerHistory",
"Name",
"Description",
"ObjectType",
"ObjectPlacement",
"Representation",
"Tag",
"PredefinedType"
],
);
}
#[test]
fn version_specific_entities_resolve_in_their_own_schema() {
assert!(
!ifc2x3().attributes("IFC2DCOMPOSITECURVE").is_empty(),
"Ifc2DCompositeCurve exists in IFC2x3"
);
assert!(
ifc4().type_def("IfcHeatFluxDensityMeasure").is_some(),
"IFC4 keeps the derived measure types"
);
}
#[test]
fn repeated_calls_return_the_same_cached_schema() {
let first = ifc4() as *const _;
let second = ifc4() as *const _;
assert_eq!(first, second, "ifc4() must not reparse on every call");
let first = ifc2x3() as *const _;
let second = ifc2x3() as *const _;
assert_eq!(first, second, "ifc2x3() must not reparse on every call");
}
#[test]
fn type_product_subtypes_keep_their_full_slot_layout() {
let schema = ifc4();
assert_eq!(
schema.attribute_names("IFCTYPEPRODUCT"),
[
"GlobalId",
"OwnerHistory",
"Name",
"Description",
"ApplicableOccurrence",
"HasPropertySets",
"RepresentationMaps",
"Tag"
],
);
assert_eq!(
schema.attribute_names("IFCWALLTYPE"),
[
"GlobalId",
"OwnerHistory",
"Name",
"Description",
"ApplicableOccurrence",
"HasPropertySets",
"RepresentationMaps",
"Tag",
"ElementType",
"PredefinedType"
],
"ElementType sits at 8, not 6"
);
}
#[test]
fn ifc2x3_type_product_keeps_its_full_slot_layout() {
assert_eq!(
ifc2x3().attribute_names("IFCTYPEPRODUCT"),
[
"GlobalId",
"OwnerHistory",
"Name",
"Description",
"ApplicableOccurrence",
"HasPropertySets",
"RepresentationMaps",
"Tag"
],
);
}
#[test]
fn every_inline_unique_aggregate_survives_parsing() {
let schema = ifc4();
assert!(
schema
.attribute_names("IFCGRID")
.contains(&"PredefinedType"),
"IfcGrid declares UAxes/VAxes/WAxes with inline UNIQUE"
);
assert_eq!(schema.attribute_names("IFCPOLYLOOP"), ["Polygon"]);
assert_eq!(
schema.attribute_names("IFCPROPERTYTABLEVALUE")[7],
"CurveInterpolation",
"slot 7 after two inherited IfcProperty slots"
);
}
#[test]
fn the_generator_guards_match_the_bundled_artifacts() {
let source = include_str!("../tools/generate.rs");
let expected: Vec<(String, usize, usize)> = source
.split("Target {")
.skip(1)
.filter_map(|block| {
let field = |key: &str| -> Option<&str> {
let start = block.find(key)? + key.len();
let rest = &block[start..];
let end = rest.find(',')?;
Some(rest[..end].trim())
};
Some((
field("selector:")?.trim_matches('"').to_owned(),
field("entities:")?.parse().ok()?,
field("types:")?.parse().ok()?,
))
})
.collect();
let bundled = [
("ifc2x3", SchemaVersion::Ifc2x3, ifc2x3()),
("ifc4", SchemaVersion::Ifc4, ifc4()),
("ifc4x1", SchemaVersion::Ifc4x1, ifc4x1()),
("ifc4x2", SchemaVersion::Ifc4x2, ifc4x2()),
("ifc4x3", SchemaVersion::Ifc4x3, ifc4x3()),
];
assert_eq!(expected.len(), bundled.len(), "one generator target each");
for ((selector, entities, types), (name, version, schema)) in expected.iter().zip(bundled) {
assert_eq!(selector, name, "generator target order");
assert_eq!(
(*entities, *types),
(schema.entity_count(), schema.type_count()),
"{name} generator guard vs the committed artifact"
);
assert_eq!(
(*entities, *types),
(
version.expected_entity_count(),
version.expected_type_count()
),
"{name} generator guard vs SchemaVersion"
);
assert_eq!(schema.version(), Some(version), "{name} declared name");
}
}
#[test]
fn inverse_and_unique_declarations_match_the_normative_counts() {
for (schema, inverses, unique) in [
(ifc2x3(), 115, 17),
(ifc4(), 153, 4),
(ifc4x1(), 158, 4),
(ifc4x2(), 160, 4),
(ifc4x3(), 165, 4),
] {
let counted: usize = schema.entities().map(|e| e.inverses.len()).sum();
assert_eq!(counted, inverses, "{} INVERSE", schema.name());
let counted: usize = schema.entities().map(|e| e.unique_rules.len()).sum();
assert_eq!(counted, unique, "{} UNIQUE", schema.name());
for entity in schema.entities() {
for attribute in &entity.attributes {
assert_eq!(
attribute.aggregate,
!attribute.aggregation.is_empty(),
"{} {}.{}",
schema.name(),
entity.name,
attribute.name
);
}
}
}
let root = ifc4().entity("IfcRoot").unwrap();
assert_eq!(root.unique_rules[0].label.as_deref(), Some("UR1"));
assert_eq!(root.unique_rules[0].attributes, ["GlobalId"]);
let object = ifc4().entity("IfcObjectDefinition").unwrap();
let decomposes = object
.inverses
.iter()
.find(|inverse| inverse.name == "Decomposes")
.unwrap();
assert_eq!(decomposes.entity, "IfcRelAggregates");
assert_eq!(decomposes.for_attribute, "RelatedObjects");
let set = decomposes.aggregation.as_ref().unwrap();
assert_eq!(
(set.kind, &set.lower, &set.upper),
(
crate::AggregateKind::Set,
&crate::Bound::Integer(0),
&crate::Bound::Integer(1)
)
);
}
#[test]
fn aggregate_bounds_and_nesting_are_bundled() {
use crate::{AggregateKind, Bound};
let coords = ifc4()
.attributes("IfcCartesianPointList3D")
.into_iter()
.find(|attribute| attribute.name == "CoordList")
.unwrap()
.clone();
assert_eq!(coords.type_name, "IfcLengthMeasure");
assert_eq!(coords.aggregation.len(), 2);
assert_eq!(coords.aggregation[0].kind, AggregateKind::List);
assert_eq!(coords.aggregation[0].lower, Bound::Integer(1));
assert_eq!(coords.aggregation[0].upper, Bound::Unbounded);
assert_eq!(coords.aggregation[1].lower, Bound::Integer(3));
assert_eq!(coords.aggregation[1].upper, Bound::Integer(3));
let point = ifc2x3()
.attributes("IfcCartesianPoint")
.into_iter()
.find(|attribute| attribute.name == "Coordinates")
.unwrap()
.clone();
assert_eq!(point.type_name, "IfcLengthMeasure");
assert_eq!(point.aggregation.len(), 1);
assert_eq!(
(&point.aggregation[0].lower, &point.aggregation[0].upper),
(&Bound::Integer(1), &Bound::Integer(3))
);
let polyline = ifc4()
.attributes("IfcPolyLoop")
.into_iter()
.find(|attribute| attribute.name == "Polygon")
.unwrap()
.clone();
assert!(polyline.aggregation[0].unique);
assert_eq!(polyline.aggregation[0].lower, Bound::Integer(3));
}
#[test]
fn the_intermediate_releases_are_distinct_tables() {
for schema in [ifc4x1(), ifc4x2()] {
for neighbour in [ifc4(), ifc4x3()] {
assert_ne!(schema.entity_count(), neighbour.entity_count());
}
}
assert_ne!(ifc4x1().entity_count(), ifc4x2().entity_count());
assert!(ifc4().entity("IfcAlignment").is_none());
assert!(ifc4x1().entity("IfcAlignment").is_some());
assert!(ifc4x1().entity("IfcAlignmentCurve").is_some());
assert!(ifc4x1().entity("IfcBridge").is_none());
assert!(ifc4x2().entity("IfcBridge").is_some());
assert!(ifc4x2().entity("IfcBuiltElement").is_none());
assert!(ifc4x3().entity("IfcAlignmentCurve").is_none());
assert_eq!(
for_version(SchemaVersion::Ifc4x1).map(Schema::name),
Ok("IFC4X1")
);
assert_eq!(
for_version(SchemaVersion::Ifc4x2).map(Schema::name),
Ok("IFC4X2")
);
assert!(std::ptr::eq(ifc4x1(), ifc4x1()), "constructor must cache");
}
}
#[cfg(all(test, feature = "ifc4", not(feature = "ifc4x3")))]
mod single_release_tests {
use super::*;
#[test]
fn an_unbundled_release_is_refused_with_not_bundled() {
assert_eq!(
for_version(SchemaVersion::Ifc4).map(Schema::name),
Ok("IFC4")
);
let refused = for_version(SchemaVersion::Ifc4x3).expect_err("not compiled in");
assert_eq!(
refused,
NotBundled {
version: SchemaVersion::Ifc4x3
}
);
assert!(!SchemaVersion::Ifc4x3.is_bundled());
assert!(SchemaVersion::Ifc4.is_bundled());
assert!(
refused.to_string().contains("`ifc4x3` feature"),
"{refused}"
);
assert_eq!(
SchemaVersion::from_header_token("IFC4X3"),
Some(SchemaVersion::Ifc4x3)
);
}
}