Skip to main content

ifc_schema/
version.rs

1//! Which IFC schema version a file declares.
2//!
3//! The token comes from the `FILE_SCHEMA` entry in a STEP header. Real files
4//! carry variants (`IFC4X3_ADD2` as well as `IFC4X3`), so matching is explicit
5//! rather than a prefix test: `IFC4X1` and `IFC4X2` are their own releases,
6//! never a prefix match onto IFC4. Each release's accepted tokens are its
7//! `SCHEMA` name as declared in its normative EXPRESS file (`SCHEMA IFC4X1;`,
8//! `SCHEMA IFC4X2;`), which is what ISO 10303-21 writes into `FILE_SCHEMA`.
9
10/// Which IFC schema version a table describes.
11///
12/// `#[non_exhaustive]`: a release added later is a new variant, and a
13/// consumer's `match` must say what it does with a release it has not
14/// proven -- refuse it, never alias it to a neighbour.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[non_exhaustive]
17pub enum SchemaVersion {
18    /// IFC2x3 TC1 — 653 entities. Still the most common in the wild.
19    Ifc2x3,
20    /// IFC4 ADD2 TC1 — 776 entities. The ISO-standard release.
21    Ifc4,
22    /// IFC4x1 FINAL — 801 entities. Adds alignment (`IfcAlignment` and its
23    /// curves) to IFC4.
24    Ifc4x1,
25    /// IFC4x2 FINAL — 816 entities. Adds bridges (`IfcBridge`, `IfcFacility`)
26    /// to IFC4x1.
27    Ifc4x2,
28    /// IFC4x3 ADD2 — 876 entities. Adds alignment and civil infrastructure.
29    Ifc4x3,
30}
31
32impl SchemaVersion {
33    /// Every version this crate knows, oldest first.
34    ///
35    /// Which of them have bundled tables in a given build is a separate
36    /// question; see `for_version`.
37    pub const ALL: [Self; 5] = [
38        Self::Ifc2x3,
39        Self::Ifc4,
40        Self::Ifc4x1,
41        Self::Ifc4x2,
42        Self::Ifc4x3,
43    ];
44
45    /// Exact published release identifier for the bundled structural table.
46    #[must_use]
47    pub const fn release_id(self) -> &'static str {
48        match self {
49            Self::Ifc2x3 => "IFC2X3_TC1",
50            Self::Ifc4 => "IFC4_ADD2_TC1",
51            Self::Ifc4x1 => "IFC4X1_FINAL",
52            Self::Ifc4x2 => "IFC4X2_FINAL",
53            Self::Ifc4x3 => "IFC4X3_ADD2",
54        }
55    }
56
57    /// Entity count asserted when the bundled artifact is generated.
58    #[must_use]
59    pub const fn expected_entity_count(self) -> usize {
60        match self {
61            Self::Ifc2x3 => 653,
62            Self::Ifc4 => 776,
63            Self::Ifc4x1 => 801,
64            Self::Ifc4x2 => 816,
65            Self::Ifc4x3 => 876,
66        }
67    }
68
69    /// Defined-type count asserted when the bundled artifact is generated.
70    #[must_use]
71    pub const fn expected_type_count(self) -> usize {
72        match self {
73            Self::Ifc2x3 => 327,
74            Self::Ifc4 => 397,
75            Self::Ifc4x1 => 400,
76            Self::Ifc4x2 => 407,
77            Self::Ifc4x3 => 436,
78        }
79    }
80
81    /// Header tokens accepted for this release, excluding case-only variants.
82    #[must_use]
83    pub const fn header_tokens(self) -> &'static [&'static str] {
84        match self {
85            Self::Ifc2x3 => &["IFC2X3"],
86            Self::Ifc4 => &["IFC4"],
87            Self::Ifc4x1 => &["IFC4X1"],
88            Self::Ifc4x2 => &["IFC4X2"],
89            Self::Ifc4x3 => &["IFC4X3", "IFC4X3_ADD2"],
90        }
91    }
92
93    /// The `ifc-schema` cargo feature that bundles this release's table.
94    #[must_use]
95    pub const fn feature_name(self) -> &'static str {
96        match self {
97            Self::Ifc2x3 => "ifc2x3",
98            Self::Ifc4 => "ifc4",
99            Self::Ifc4x1 => "ifc4x1",
100            Self::Ifc4x2 => "ifc4x2",
101            Self::Ifc4x3 => "ifc4x3",
102        }
103    }
104
105    /// Whether this build bundles this release's table, i.e. whether
106    /// `for_version(self)` succeeds.
107    #[must_use]
108    pub const fn is_bundled(self) -> bool {
109        match self {
110            Self::Ifc2x3 => cfg!(feature = "ifc2x3"),
111            Self::Ifc4 => cfg!(feature = "ifc4"),
112            Self::Ifc4x1 => cfg!(feature = "ifc4x1"),
113            Self::Ifc4x2 => cfg!(feature = "ifc4x2"),
114            Self::Ifc4x3 => cfg!(feature = "ifc4x3"),
115        }
116    }
117
118    /// Parse the token found in a STEP file's `FILE_SCHEMA` header entry.
119    pub fn from_header_token(token: &str) -> Option<Self> {
120        match token.trim().to_ascii_uppercase().as_str() {
121            "IFC2X3" => Some(Self::Ifc2x3),
122            "IFC4" => Some(Self::Ifc4),
123            "IFC4X1" => Some(Self::Ifc4x1),
124            "IFC4X2" => Some(Self::Ifc4x2),
125            "IFC4X3" | "IFC4X3_ADD2" => Some(Self::Ifc4x3),
126            _ => None,
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn recognizes_the_schema_tokens_our_fixtures_carry() {
137        assert_eq!(
138            SchemaVersion::from_header_token("IFC4"),
139            Some(SchemaVersion::Ifc4)
140        );
141        assert_eq!(
142            SchemaVersion::from_header_token("ifc2x3"),
143            Some(SchemaVersion::Ifc2x3)
144        );
145        assert_eq!(SchemaVersion::from_header_token("STEP"), None);
146    }
147
148    /// Every declared header token maps back to its own version, and the
149    /// release identifiers are distinct: IFC4X1 and IFC4X2 are neither IFC4
150    /// nor IFC4X3.
151    #[test]
152    fn header_tokens_and_release_ids_round_trip_for_every_version() {
153        for version in SchemaVersion::ALL {
154            for token in version.header_tokens() {
155                assert_eq!(SchemaVersion::from_header_token(token), Some(version));
156                assert_eq!(
157                    SchemaVersion::from_header_token(&token.to_ascii_lowercase()),
158                    Some(version)
159                );
160            }
161        }
162        assert_eq!(
163            SchemaVersion::from_header_token("IFC4X1"),
164            Some(SchemaVersion::Ifc4x1)
165        );
166        assert_eq!(
167            SchemaVersion::from_header_token(" ifc4x2 "),
168            Some(SchemaVersion::Ifc4x2)
169        );
170        assert_eq!(SchemaVersion::Ifc4x1.release_id(), "IFC4X1_FINAL");
171        assert_eq!(SchemaVersion::Ifc4x2.release_id(), "IFC4X2_FINAL");
172        let ids: std::collections::BTreeSet<_> =
173            SchemaVersion::ALL.iter().map(|v| v.release_id()).collect();
174        assert_eq!(ids.len(), SchemaVersion::ALL.len());
175    }
176}