1#[cfg(feature = "artifact")]
32use std::sync::OnceLock;
33
34#[cfg(feature = "artifact")]
35use crate::artifact::decode_schema;
36use crate::registry::Schema;
37use crate::version::SchemaVersion;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46#[non_exhaustive]
47pub struct NotBundled {
48 pub version: SchemaVersion,
50}
51
52impl std::fmt::Display for NotBundled {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(
55 f,
56 "IFC release {} is not compiled into this build (enable the `{}` feature of ifc-schema)",
57 self.version.release_id(),
58 self.version.feature_name()
59 )
60 }
61}
62
63impl std::error::Error for NotBundled {}
64
65macro_rules! bundled_release {
67 ($feature:literal, $cell:ident, $name:ident, $file:literal, $label:literal, $doc:literal) => {
68 #[cfg(feature = $feature)]
69 static $cell: OnceLock<Schema> = OnceLock::new();
70
71 #[doc = $doc]
72 #[doc = concat!("Requires the `", $feature, "` feature (on by default).")]
75 #[cfg(feature = $feature)]
76 #[must_use]
77 pub fn $name() -> &'static Schema {
78 $cell.get_or_init(|| {
79 decode_schema(include_bytes!($file)).expect(concat!(
80 "the bundled ",
81 $label,
82 " artifact is produced and verified by this crate's own build"
83 ))
84 })
85 }
86 };
87}
88
89bundled_release!(
90 "ifc2x3",
91 IFC2X3,
92 ifc2x3,
93 "../data/ifc2x3-tc1.bin",
94 "IFC2x3",
95 "The bundled IFC2x3 TC1 schema (653 entities, 327 types).\n\n\
96 Still the most common schema in the wild. Its layouts differ from IFC4 \
97 in ways that silently corrupt a reader that assumes the newer tables: \
98 `IfcWallStandardCase` has 8 attributes here and 9 in IFC4, because IFC4 \
99 inserts `PredefinedType`."
100);
101bundled_release!(
102 "ifc4",
103 IFC4,
104 ifc4,
105 "../data/ifc4-add2-tc1.bin",
106 "IFC4",
107 "The bundled IFC4 ADD2 TC1 schema (776 entities, 397 types).\n\n\
108 Building it costs a `bincode` decode of a committed artifact: the \
109 `IFC4.exp` EXPRESS source is never read at runtime and is not in the \
110 published crate. Custom schema files remain available through \
111 `Schema::from_express` (the `express` feature) or [`Schema::new`]."
112);
113bundled_release!(
114 "ifc4x1",
115 IFC4X1,
116 ifc4x1,
117 "../data/ifc4x1-final.bin",
118 "IFC4X1",
119 "The bundled IFC4X1 FINAL schema (801 entities, 400 types).\n\n\
120 Its own artifact: IFC4X1 adds the alignment entities to IFC4 and is not \
121 an alias for either IFC4 or IFC4X3."
122);
123bundled_release!(
124 "ifc4x2",
125 IFC4X2,
126 ifc4x2,
127 "../data/ifc4x2-final.bin",
128 "IFC4X2",
129 "The bundled IFC4X2 FINAL schema (816 entities, 407 types).\n\n\
130 Its own artifact: IFC4X2 adds bridges to IFC4X1 and is not an alias for \
131 IFC4 or IFC4X3."
132);
133bundled_release!(
134 "ifc4x3",
135 IFC4X3,
136 ifc4x3,
137 "../data/ifc4x3-add2.bin",
138 "IFC4X3",
139 "The bundled IFC4X3 ADD2 schema (876 entities, 436 types).\n\n\
140 Its own artifact. It is never an alias for IFC4: renamed and civil \
141 entities require the declared tables."
142);
143
144pub fn for_version(version: SchemaVersion) -> Result<&'static Schema, NotBundled> {
155 match version {
156 #[cfg(feature = "ifc2x3")]
157 SchemaVersion::Ifc2x3 => Ok(ifc2x3()),
158 #[cfg(feature = "ifc4")]
159 SchemaVersion::Ifc4 => Ok(ifc4()),
160 #[cfg(feature = "ifc4x1")]
161 SchemaVersion::Ifc4x1 => Ok(ifc4x1()),
162 #[cfg(feature = "ifc4x2")]
163 SchemaVersion::Ifc4x2 => Ok(ifc4x2()),
164 #[cfg(feature = "ifc4x3")]
165 SchemaVersion::Ifc4x3 => Ok(ifc4x3()),
166 #[allow(unreachable_patterns)]
167 _ => Err(NotBundled { version }),
168 }
169}
170
171#[cfg(all(
172 test,
173 feature = "ifc2x3",
174 feature = "ifc4",
175 feature = "ifc4x1",
176 feature = "ifc4x2",
177 feature = "ifc4x3"
178))]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn bundled_ifc4_matches_the_normative_entity_and_type_counts() {
184 let schema = ifc4();
185 assert_eq!(schema.entity_count(), 776, "IFC4 ADD2 TC1 entity count");
186 assert_eq!(schema.type_count(), 397, "IFC4 ADD2 TC1 type count");
187 }
188
189 #[test]
190 fn bundled_ifc4x3_matches_the_normative_entity_and_type_counts() {
191 let schema = ifc4x3();
192 assert_eq!(schema.entity_count(), 876, "IFC4X3 ADD2 entity count");
193 assert_eq!(schema.type_count(), 436, "IFC4X3 ADD2 type count");
194 assert!(schema.entity("IfcBuiltElement").is_some());
195 assert!(schema.entity("IfcBuildingElement").is_none());
196 assert!(std::ptr::eq(schema, ifc4x3()), "constructor must cache");
197 }
198
199 #[test]
200 fn bundled_ifc2x3_matches_the_normative_entity_and_type_counts() {
201 let schema = ifc2x3();
202 assert_eq!(schema.entity_count(), 653, "IFC2x3 TC1 entity count");
203 assert_eq!(schema.type_count(), 327, "IFC2x3 TC1 type count");
204 }
205
206 #[test]
207 fn bundled_ifc4_resolves_the_deep_inheritance_chain() {
208 let schema = ifc4();
209 assert!(schema.is_a("IFCWALL", "IfcRoot"), "wall is a root");
210 assert!(schema.is_a("IFCWALL", "IfcProduct"), "wall is a product");
211 assert_eq!(
212 &schema.attribute_names("IFCWALL")[..4],
213 ["GlobalId", "OwnerHistory", "Name", "Description"],
214 "IfcRoot's slots must come first"
215 );
216 }
217
218 #[test]
220 fn version_lookup_returns_the_matching_table() {
221 assert_eq!(
222 for_version(SchemaVersion::Ifc4).map(|s| s.entity_count()),
223 Ok(776)
224 );
225 assert_eq!(
226 for_version(SchemaVersion::Ifc2x3).map(|s| s.entity_count()),
227 Ok(653)
228 );
229 assert_eq!(
230 for_version(SchemaVersion::Ifc4x3).map(|s| s.entity_count()),
231 Ok(876),
232 "IFC4X3 must select its own bundled tables"
233 );
234 }
235
236 #[test]
242 fn the_ifc2x3_and_ifc4_bundles_are_not_the_same_table() {
243 assert_eq!(
245 ifc2x3().attribute_names("IFCWALLSTANDARDCASE"),
246 [
247 "GlobalId",
248 "OwnerHistory",
249 "Name",
250 "Description",
251 "ObjectType",
252 "ObjectPlacement",
253 "Representation",
254 "Tag"
255 ],
256 );
257 assert_eq!(
258 ifc4().attribute_names("IFCWALLSTANDARDCASE"),
259 [
260 "GlobalId",
261 "OwnerHistory",
262 "Name",
263 "Description",
264 "ObjectType",
265 "ObjectPlacement",
266 "Representation",
267 "Tag",
268 "PredefinedType"
269 ],
270 );
271 }
272
273 #[test]
275 fn version_specific_entities_resolve_in_their_own_schema() {
276 assert!(
277 !ifc2x3().attributes("IFC2DCOMPOSITECURVE").is_empty(),
278 "Ifc2DCompositeCurve exists in IFC2x3"
279 );
280 assert!(
281 ifc4().type_def("IfcHeatFluxDensityMeasure").is_some(),
282 "IFC4 keeps the derived measure types"
283 );
284 }
285
286 #[test]
287 fn repeated_calls_return_the_same_cached_schema() {
288 let first = ifc4() as *const _;
289 let second = ifc4() as *const _;
290 assert_eq!(first, second, "ifc4() must not reparse on every call");
291 let first = ifc2x3() as *const _;
292 let second = ifc2x3() as *const _;
293 assert_eq!(first, second, "ifc2x3() must not reparse on every call");
294 }
295
296 #[test]
307 fn type_product_subtypes_keep_their_full_slot_layout() {
308 let schema = ifc4();
309 assert_eq!(
310 schema.attribute_names("IFCTYPEPRODUCT"),
311 [
312 "GlobalId",
313 "OwnerHistory",
314 "Name",
315 "Description",
316 "ApplicableOccurrence",
317 "HasPropertySets",
318 "RepresentationMaps",
319 "Tag"
320 ],
321 );
322 assert_eq!(
323 schema.attribute_names("IFCWALLTYPE"),
324 [
325 "GlobalId",
326 "OwnerHistory",
327 "Name",
328 "Description",
329 "ApplicableOccurrence",
330 "HasPropertySets",
331 "RepresentationMaps",
332 "Tag",
333 "ElementType",
334 "PredefinedType"
335 ],
336 "ElementType sits at 8, not 6"
337 );
338 }
339
340 #[test]
342 fn ifc2x3_type_product_keeps_its_full_slot_layout() {
343 assert_eq!(
344 ifc2x3().attribute_names("IFCTYPEPRODUCT"),
345 [
346 "GlobalId",
347 "OwnerHistory",
348 "Name",
349 "Description",
350 "ApplicableOccurrence",
351 "HasPropertySets",
352 "RepresentationMaps",
353 "Tag"
354 ],
355 );
356 }
357
358 #[test]
360 fn every_inline_unique_aggregate_survives_parsing() {
361 let schema = ifc4();
362 assert!(
363 schema
364 .attribute_names("IFCGRID")
365 .contains(&"PredefinedType"),
366 "IfcGrid declares UAxes/VAxes/WAxes with inline UNIQUE"
367 );
368 assert_eq!(schema.attribute_names("IFCPOLYLOOP"), ["Polygon"]);
369 assert_eq!(
370 schema.attribute_names("IFCPROPERTYTABLEVALUE")[7],
371 "CurveInterpolation",
372 "slot 7 after two inherited IfcProperty slots"
373 );
374 }
375
376 #[test]
385 fn the_generator_guards_match_the_bundled_artifacts() {
386 let source = include_str!("../tools/generate.rs");
388 let expected: Vec<(String, usize, usize)> = source
389 .split("Target {")
390 .skip(1)
391 .filter_map(|block| {
392 let field = |key: &str| -> Option<&str> {
393 let start = block.find(key)? + key.len();
394 let rest = &block[start..];
395 let end = rest.find(',')?;
396 Some(rest[..end].trim())
397 };
398 Some((
399 field("selector:")?.trim_matches('"').to_owned(),
400 field("entities:")?.parse().ok()?,
401 field("types:")?.parse().ok()?,
402 ))
403 })
404 .collect();
405 let bundled = [
406 ("ifc2x3", SchemaVersion::Ifc2x3, ifc2x3()),
407 ("ifc4", SchemaVersion::Ifc4, ifc4()),
408 ("ifc4x1", SchemaVersion::Ifc4x1, ifc4x1()),
409 ("ifc4x2", SchemaVersion::Ifc4x2, ifc4x2()),
410 ("ifc4x3", SchemaVersion::Ifc4x3, ifc4x3()),
411 ];
412 assert_eq!(expected.len(), bundled.len(), "one generator target each");
413 for ((selector, entities, types), (name, version, schema)) in expected.iter().zip(bundled) {
414 assert_eq!(selector, name, "generator target order");
415 assert_eq!(
416 (*entities, *types),
417 (schema.entity_count(), schema.type_count()),
418 "{name} generator guard vs the committed artifact"
419 );
420 assert_eq!(
421 (*entities, *types),
422 (
423 version.expected_entity_count(),
424 version.expected_type_count()
425 ),
426 "{name} generator guard vs SchemaVersion"
427 );
428 assert_eq!(schema.version(), Some(version), "{name} declared name");
429 }
430 }
431
432 #[test]
437 fn inverse_and_unique_declarations_match_the_normative_counts() {
438 for (schema, inverses, unique) in [
439 (ifc2x3(), 115, 17),
440 (ifc4(), 153, 4),
441 (ifc4x1(), 158, 4),
442 (ifc4x2(), 160, 4),
443 (ifc4x3(), 165, 4),
444 ] {
445 let counted: usize = schema.entities().map(|e| e.inverses.len()).sum();
446 assert_eq!(counted, inverses, "{} INVERSE", schema.name());
447 let counted: usize = schema.entities().map(|e| e.unique_rules.len()).sum();
448 assert_eq!(counted, unique, "{} UNIQUE", schema.name());
449 for entity in schema.entities() {
451 for attribute in &entity.attributes {
452 assert_eq!(
453 attribute.aggregate,
454 !attribute.aggregation.is_empty(),
455 "{} {}.{}",
456 schema.name(),
457 entity.name,
458 attribute.name
459 );
460 }
461 }
462 }
463 let root = ifc4().entity("IfcRoot").unwrap();
464 assert_eq!(root.unique_rules[0].label.as_deref(), Some("UR1"));
465 assert_eq!(root.unique_rules[0].attributes, ["GlobalId"]);
466 let object = ifc4().entity("IfcObjectDefinition").unwrap();
467 let decomposes = object
468 .inverses
469 .iter()
470 .find(|inverse| inverse.name == "Decomposes")
471 .unwrap();
472 assert_eq!(decomposes.entity, "IfcRelAggregates");
473 assert_eq!(decomposes.for_attribute, "RelatedObjects");
474 let set = decomposes.aggregation.as_ref().unwrap();
475 assert_eq!(
476 (set.kind, &set.lower, &set.upper),
477 (
478 crate::AggregateKind::Set,
479 &crate::Bound::Integer(0),
480 &crate::Bound::Integer(1)
481 )
482 );
483 }
484
485 #[test]
488 fn aggregate_bounds_and_nesting_are_bundled() {
489 use crate::{AggregateKind, Bound};
490 let coords = ifc4()
491 .attributes("IfcCartesianPointList3D")
492 .into_iter()
493 .find(|attribute| attribute.name == "CoordList")
494 .unwrap()
495 .clone();
496 assert_eq!(coords.type_name, "IfcLengthMeasure");
497 assert_eq!(coords.aggregation.len(), 2);
498 assert_eq!(coords.aggregation[0].kind, AggregateKind::List);
499 assert_eq!(coords.aggregation[0].lower, Bound::Integer(1));
500 assert_eq!(coords.aggregation[0].upper, Bound::Unbounded);
501 assert_eq!(coords.aggregation[1].lower, Bound::Integer(3));
502 assert_eq!(coords.aggregation[1].upper, Bound::Integer(3));
503
504 let point = ifc2x3()
505 .attributes("IfcCartesianPoint")
506 .into_iter()
507 .find(|attribute| attribute.name == "Coordinates")
508 .unwrap()
509 .clone();
510 assert_eq!(point.type_name, "IfcLengthMeasure");
511 assert_eq!(point.aggregation.len(), 1);
512 assert_eq!(
513 (&point.aggregation[0].lower, &point.aggregation[0].upper),
514 (&Bound::Integer(1), &Bound::Integer(3))
515 );
516
517 let polyline = ifc4()
518 .attributes("IfcPolyLoop")
519 .into_iter()
520 .find(|attribute| attribute.name == "Polygon")
521 .unwrap()
522 .clone();
523 assert!(polyline.aggregation[0].unique);
524 assert_eq!(polyline.aggregation[0].lower, Bound::Integer(3));
525 }
526
527 #[test]
531 fn the_intermediate_releases_are_distinct_tables() {
532 for schema in [ifc4x1(), ifc4x2()] {
533 for neighbour in [ifc4(), ifc4x3()] {
534 assert_ne!(schema.entity_count(), neighbour.entity_count());
535 }
536 }
537 assert_ne!(ifc4x1().entity_count(), ifc4x2().entity_count());
538
539 assert!(ifc4().entity("IfcAlignment").is_none());
540 assert!(ifc4x1().entity("IfcAlignment").is_some());
541 assert!(ifc4x1().entity("IfcAlignmentCurve").is_some());
542 assert!(ifc4x1().entity("IfcBridge").is_none());
543 assert!(ifc4x2().entity("IfcBridge").is_some());
544 assert!(ifc4x2().entity("IfcBuiltElement").is_none());
545 assert!(ifc4x3().entity("IfcAlignmentCurve").is_none());
546
547 assert_eq!(
548 for_version(SchemaVersion::Ifc4x1).map(Schema::name),
549 Ok("IFC4X1")
550 );
551 assert_eq!(
552 for_version(SchemaVersion::Ifc4x2).map(Schema::name),
553 Ok("IFC4X2")
554 );
555 assert!(std::ptr::eq(ifc4x1(), ifc4x1()), "constructor must cache");
556 }
557}
558
559#[cfg(all(test, feature = "ifc4", not(feature = "ifc4x3")))]
563mod single_release_tests {
564 use super::*;
565
566 #[test]
567 fn an_unbundled_release_is_refused_with_not_bundled() {
568 assert_eq!(
569 for_version(SchemaVersion::Ifc4).map(Schema::name),
570 Ok("IFC4")
571 );
572 let refused = for_version(SchemaVersion::Ifc4x3).expect_err("not compiled in");
573 assert_eq!(
574 refused,
575 NotBundled {
576 version: SchemaVersion::Ifc4x3
577 }
578 );
579 assert!(!SchemaVersion::Ifc4x3.is_bundled());
580 assert!(SchemaVersion::Ifc4.is_bundled());
581 assert!(
582 refused.to_string().contains("`ifc4x3` feature"),
583 "{refused}"
584 );
585 assert_eq!(
587 SchemaVersion::from_header_token("IFC4X3"),
588 Some(SchemaVersion::Ifc4x3)
589 );
590 }
591}