ifc_classification/release.rs
1//! The IFC release a model's classification records are read against.
2//!
3//! Every slot position, select, and domain comes from the bundled table of
4//! one release, so an IFC2X3 record is never decoded with IFC4 positions.
5//! Binding: a header declaring exactly `IFC2X3` binds the IFC2X3 table.
6//! Any other single declaration, and an undeclared header (an in-memory
7//! model), binds IFC4, which is the 0.2.0 behaviour. Several declarations
8//! that include IFC2X3 cannot be bound to one release and fail closed.
9
10use ifc_model::{Entity, EntityId, Model, Value};
11use ifc_schema::{for_version, Schema, SchemaVersion};
12
13use crate::{ClassificationError, ClassificationResult};
14
15/// The release a view reads against, or why none could be bound.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub(crate) enum Release {
18 /// Reads resolve against this release's bundled table.
19 Bound(SchemaVersion),
20 /// The header declares this many schemas, one of them IFC2X3.
21 Multiple(usize),
22}
23
24impl Release {
25 /// The binding of projections built without a model (`try_new`).
26 pub(crate) const LEGACY: Self = Self::Bound(SchemaVersion::Ifc4);
27
28 /// The release `model`'s header binds.
29 pub(crate) fn of(model: &Model) -> Self {
30 let is_2x3 =
31 |token: &String| SchemaVersion::from_header_token(token) == Some(SchemaVersion::Ifc2x3);
32 match model.header().schema.as_slice() {
33 [token] if is_2x3(token) => Self::Bound(SchemaVersion::Ifc2x3),
34 tokens if tokens.len() > 1 && tokens.iter().any(is_2x3) => Self::Multiple(tokens.len()),
35 _ => Self::LEGACY,
36 }
37 }
38
39 /// The bound version and its bundled table.
40 pub(crate) fn bound(self) -> ClassificationResult<(SchemaVersion, &'static Schema)> {
41 match self {
42 Self::Bound(version) => Ok((
43 version,
44 for_version(version).expect("the IFC2X3 and IFC4 tables are bundled"),
45 )),
46 Self::Multiple(schemas) => Err(ClassificationError::MultipleSchemas { schemas }),
47 }
48 }
49
50 /// Position of `attribute` on `entity` in the bound release.
51 ///
52 /// `attribute` is the IFC4 name; IFC2X3 spells two of them differently
53 /// (see [`release_name`]). An attribute or record the release does not
54 /// declare is `NotInSchema`, never a silent `None` read past the record.
55 pub(crate) fn slot(
56 self,
57 entity: &'static str,
58 id: EntityId,
59 attribute: &'static str,
60 ) -> ClassificationResult<usize> {
61 let (version, schema) = self.bound()?;
62 let name = release_name(version, entity, attribute);
63 schema
64 .attributes(entity)
65 .iter()
66 .position(|declared| declared.name.eq_ignore_ascii_case(name))
67 .ok_or(ClassificationError::NotInSchema {
68 entity,
69 id,
70 attribute,
71 schema: version,
72 })
73 }
74
75 /// [`Self::slot`] for a text accessor.
76 ///
77 /// IFC2X3 types several dates and formats as entity records (for example
78 /// `IfcClassification.EditionDate` is an `IfcCalendarDate`). Such a value
79 /// is valid, not malformed, so it is reported as `StructuredValue` with
80 /// the record id instead of being rejected as an invalid string.
81 pub(crate) fn text_slot(
82 self,
83 entity: &'static str,
84 id: EntityId,
85 record: &Entity,
86 attribute: &'static str,
87 ) -> ClassificationResult<usize> {
88 let slot = self.slot(entity, id, attribute)?;
89 if let Some(Value::Ref(target)) = record.attribute(slot) {
90 let declared = self.declared_type(entity, id, attribute)?;
91 let (_, schema) = self.bound()?;
92 if schema.entity(declared).is_some() {
93 return Err(ClassificationError::StructuredValue {
94 entity,
95 id,
96 attribute,
97 target: *target,
98 });
99 }
100 }
101 Ok(slot)
102 }
103
104 /// The type the bound release declares for `attribute` on `entity`,
105 /// for example `IfcClassificationNotationSelect` for IFC2X3
106 /// `IfcRelAssociatesClassification.RelatingClassification`.
107 pub(crate) fn declared_type(
108 self,
109 entity: &'static str,
110 id: EntityId,
111 attribute: &'static str,
112 ) -> ClassificationResult<&'static str> {
113 let slot = self.slot(entity, id, attribute)?;
114 let (_, schema) = self.bound()?;
115 Ok(schema.attributes(entity)[slot].type_name.as_str())
116 }
117
118 /// Whether `candidate` is a legal value of `attribute` on `entity`.
119 pub(crate) fn accepts(
120 self,
121 entity: &'static str,
122 id: EntityId,
123 attribute: &'static str,
124 candidate: &str,
125 ) -> ClassificationResult<bool> {
126 let declared = self.declared_type(entity, id, attribute)?;
127 let (_, schema) = self.bound()?;
128 Ok(schema.accepts_type(declared, candidate))
129 }
130}
131
132/// The name `release` gives the attribute this crate knows by its IFC4 name.
133///
134/// IFC4 renamed IFC2X3 `IfcExternalReference.ItemReference` to
135/// `Identification`, and `IfcDocumentInformation.DocumentId` to
136/// `Identification`. Both keep their position and meaning, so the IFC4
137/// accessor reads them. No other attribute is aliased.
138fn release_name(release: SchemaVersion, entity: &str, attribute: &'static str) -> &'static str {
139 match (release, entity, attribute) {
140 (SchemaVersion::Ifc2x3, "IFCDOCUMENTINFORMATION", "Identification") => "DocumentId",
141 (
142 SchemaVersion::Ifc2x3,
143 "IFCCLASSIFICATIONREFERENCE" | "IFCDOCUMENTREFERENCE" | "IFCLIBRARYREFERENCE",
144 "Identification",
145 ) => "ItemReference",
146 _ => attribute,
147 }
148}
149
150/// The IFC release `model`'s classification records are read against.
151///
152/// `IFC2X3` binds the IFC2X3 table. Any other single declaration, and a
153/// header with none, binds IFC4 (IFC4X3 is not yet read against its own
154/// table). A consumer binds its vocabulary with this answer instead of
155/// re-parsing the header.
156///
157/// # Errors
158///
159/// [`ClassificationError::MultipleSchemas`] when the header declares several
160/// schemas including IFC2X3, so no single release can be bound.
161pub fn classification_schema(model: &Model) -> ClassificationResult<SchemaVersion> {
162 Release::of(model).bound().map(|(version, _)| version)
163}