Skip to main content

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}