Skip to main content

ifc_validate/where_rule/
registry.rs

1//! Which WHERE rules this validator actually evaluates.
2//!
3//! # The honesty problem this solves
4//!
5//! IFC4 declares hundreds of `WHERE` rules as EXPRESS expressions. This
6//! validator does not have an EXPRESS expression evaluator, so it cannot
7//! check most of them. The tempting design is to check the ones it can and
8//! stay quiet about the rest -- which produces a clean report for a file
9//! nobody fully checked, and a user who believes it.
10//!
11//! Instead every rule is registered with an explicit state. Supported rules
12//! are evaluated; unsupported ones are *reported* as unsupported. A caller
13//! can therefore distinguish "this file is conformant" from "this file did
14//! not trip the subset we implement".
15//!
16//! # Why a registry rather than a list of implemented functions
17//!
18//! Rules get implemented over time. If support were implicit in which
19//! functions exist, nothing would tell a reader which rules are missing --
20//! the absence of code is invisible. The registry makes the gap a data
21//! structure that can be counted, printed, and tested against.
22
23/// Whether this validator evaluates a given rule.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Support {
26    /// Implemented and evaluated on every run.
27    Implemented,
28    /// Declared by the schema, not evaluated here.
29    ///
30    /// Carries why, so the report can say something better than "no".
31    Unsupported(&'static str),
32}
33
34/// One registered rule.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct RuleEntry {
37    /// Stable rule id, e.g. `IfcRoot.WR1` or `global.IfcSingleProjectInstance`.
38    pub id: &'static str,
39    /// The entity the rule constrains, or `None` for a global rule.
40    pub entity: Option<&'static str>,
41    /// Whether it is evaluated.
42    pub support: Support,
43}
44
45/// Reason strings, shared so the same gap reads identically everywhere.
46const NEEDS_EXPRESSIONS: &str = "requires an EXPRESS expression evaluator";
47const NEEDS_BOUNDS: &str = "requires aggregate bounds, which the schema parser does not retain";
48const NEEDS_INVERSES: &str =
49    "not implemented uniformly: IFC2X3 requires INVERSE relationship semantics, which validation does not derive";
50const NEEDS_GEOMETRY: &str = "requires geometric evaluation, which validation does not perform";
51
52/// Every rule this validator knows about, implemented or not.
53///
54/// Deliberately not exhaustive over every bundled IFC release: claiming to
55/// enumerate all rules would be its own dishonesty. It covers selected
56/// high-value predicates plus representative unsupported categories.
57pub const RULES: &[RuleEntry] = &[
58    RuleEntry {
59        id: "global.IfcSingleProjectInstance",
60        entity: None,
61        support: Support::Implemented,
62    },
63    // IfcRoot constrains GlobalId with `UNIQUE UR1`, not a WHERE rule. It is
64    // registered here under its global id rather than as `IfcRoot.UR1`: the
65    // check is file-wide, and two entries for one check would double-report.
66    RuleEntry {
67        id: "global.UniqueGlobalId",
68        entity: None,
69        support: Support::Implemented,
70    },
71    RuleEntry {
72        id: "IfcRelDefinesByProperties.NoRelatedTypeObject",
73        entity: Some("IfcRelDefinesByProperties"),
74        support: Support::Implemented,
75    },
76    RuleEntry {
77        id: "IfcExternalReference.WR1",
78        entity: Some("IfcExternalReference"),
79        support: Support::Implemented,
80    },
81    RuleEntry {
82        id: "IfcRelSequence.WR1",
83        entity: Some("IfcRelSequence"),
84        support: Support::Implemented,
85    },
86    RuleEntry {
87        id: "IfcRelSequence.AvoidInconsistentSequence",
88        entity: Some("IfcRelSequence"),
89        support: Support::Implemented,
90    },
91    RuleEntry {
92        id: "IfcRelAggregates.NoSelfReference",
93        entity: Some("IfcRelAggregates"),
94        support: Support::Implemented,
95    },
96    RuleEntry {
97        id: "IfcRelNests.NoSelfReference",
98        entity: Some("IfcRelNests"),
99        support: Support::Implemented,
100    },
101    RuleEntry {
102        id: "IfcMaterialLayer.NormalizedPriority",
103        entity: Some("IfcMaterialLayer"),
104        support: Support::Implemented,
105    },
106    RuleEntry {
107        id: "IfcRelAssignsToActor.NoSelfReference",
108        entity: Some("IfcRelAssignsToActor"),
109        support: Support::Implemented,
110    },
111    RuleEntry {
112        id: "IfcRelAssignsToProcess.NoSelfReference",
113        entity: Some("IfcRelAssignsToProcess"),
114        support: Support::Implemented,
115    },
116    RuleEntry {
117        id: "IfcRelAssignsToProduct.NoSelfReference",
118        entity: Some("IfcRelAssignsToProduct"),
119        support: Support::Implemented,
120    },
121    RuleEntry {
122        id: "IfcRelAssignsToGroupByFactor.NoSelfReference",
123        entity: Some("IfcRelAssignsToGroupByFactor"),
124        support: Support::Implemented,
125    },
126    RuleEntry {
127        id: "IfcRelConnectsPathElements.NormalizedRelatingPriorities",
128        entity: Some("IfcRelConnectsPathElements"),
129        support: Support::Implemented,
130    },
131    RuleEntry {
132        id: "IfcRelConnectsPathElements.NormalizedRelatedPriorities",
133        entity: Some("IfcRelConnectsPathElements"),
134        support: Support::Implemented,
135    },
136    RuleEntry {
137        id: "IfcRelSpaceBoundary.CorrectPhysOrVirt",
138        entity: Some("IfcRelSpaceBoundary"),
139        support: Support::Implemented,
140    },
141    RuleEntry {
142        id: "IfcDocumentReference.WR1",
143        entity: Some("IfcDocumentReference"),
144        support: Support::Unsupported(NEEDS_INVERSES),
145    },
146    RuleEntry {
147        id: "IfcRepresentationContextSameWCS",
148        entity: None,
149        support: Support::Unsupported(NEEDS_GEOMETRY),
150    },
151    RuleEntry {
152        id: "IfcPolyLoop.WR21",
153        entity: Some("IfcPolyLoop"),
154        support: Support::Unsupported(NEEDS_BOUNDS),
155    },
156    RuleEntry {
157        id: "IfcPhysicalSimpleQuantity.WR21",
158        entity: Some("IfcQuantityLength"),
159        support: Support::Unsupported(NEEDS_EXPRESSIONS),
160    },
161    RuleEntry {
162        id: "IfcZone.WR1",
163        entity: Some("IfcZone"),
164        support: Support::Unsupported(NEEDS_EXPRESSIONS),
165    },
166];
167
168/// The registered entry for `id`, if there is one.
169#[must_use]
170pub fn lookup(id: &str) -> Option<&'static RuleEntry> {
171    RULES.iter().find(|entry| entry.id == id)
172}
173
174/// Every rule this validator does not evaluate.
175pub fn unsupported() -> impl Iterator<Item = &'static RuleEntry> {
176    RULES
177        .iter()
178        .filter(|entry| matches!(entry.support, Support::Unsupported(_)))
179}
180
181/// Every rule this validator does evaluate.
182pub fn implemented() -> impl Iterator<Item = &'static RuleEntry> {
183    RULES
184        .iter()
185        .filter(|entry| matches!(entry.support, Support::Implemented))
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    /// Rule ids must be unique: they are what callers suppress on.
193    #[test]
194    fn rule_ids_are_unique() {
195        let mut ids: Vec<&str> = RULES.iter().map(|entry| entry.id).collect();
196        ids.sort_unstable();
197        let count = ids.len();
198        ids.dedup();
199        assert_eq!(ids.len(), count, "duplicate rule id in the registry");
200    }
201
202    /// The registry must actually contain unevaluated rules.
203    ///
204    /// If this ever reports zero, either every IFC4 rule is implemented -- it
205    /// is not -- or rules are being dropped from the registry instead of
206    /// being marked unsupported, which is exactly the dishonesty the registry
207    /// exists to prevent.
208    #[test]
209    fn the_registry_admits_what_it_cannot_check() {
210        assert!(
211            unsupported().count() > 0,
212            "a validator claiming full WHERE-rule coverage is lying"
213        );
214    }
215
216    #[test]
217    fn unsupported_boundaries_remain_explicit() {
218        let reasons: Vec<_> = unsupported()
219            .filter_map(|entry| match entry.support {
220                Support::Unsupported(reason) => Some(reason),
221                Support::Implemented => None,
222            })
223            .collect();
224        for required in ["aggregate bounds", "EXPRESS expression", "INVERSE"] {
225            assert!(
226                reasons.iter().any(|reason| reason.contains(required)),
227                "missing explicit unsupported boundary for {required}: {reasons:?}"
228            );
229        }
230    }
231}