Skip to main content

ifc_validate/structure/
unique.rs

1//! EXPRESS `UNIQUE` rules (#111).
2//!
3//! A `UNIQUE` clause states that the named attributes are unique, jointly,
4//! across every instance of the declaring entity and its subtypes: IFC4's
5//! `IfcApplication.UR2` forbids two applications sharing both
6//! `ApplicationFullName` and `Version`. The schema tables record each clause
7//! with its label, so every one the declared release states is checked,
8//! against that release's own clauses (IFC2X3 declares 17, IFC4 and IFC4X3
9//! four).
10//!
11//! `IfcRoot.UR1` (unique `GlobalId`) is the one clause checked elsewhere: it
12//! is registered as the `global.UniqueGlobalId` rule, whose id callers
13//! already filter on, and checking it here too would report every duplicate
14//! twice.
15//!
16//! An instance whose attribute in the clause is unset (`$`) or derived (`*`)
17//! takes no part: its value is indeterminate, so it cannot be shown to
18//! collide with another.
19
20use std::collections::HashMap;
21
22use ifc_model::{EntityId, Model, Value};
23use ifc_schema::Schema;
24
25use crate::report::{Finding, Path, Report};
26
27/// The clause checked by the `global.UniqueGlobalId` where-rule instead.
28const CHECKED_AS_GLOBAL_RULE: (&str, &str) = ("IfcRoot", "UR1");
29
30/// Reports every instance that repeats another's values for a `UNIQUE`
31/// clause of the declared release.
32pub fn unique_rules(model: &Model, schema: &Schema, report: &mut Report) {
33    for declaring in schema.entities() {
34        for rule in &declaring.unique_rules {
35            let label = rule.label.as_deref().unwrap_or("unlabelled");
36            if declaring
37                .name
38                .eq_ignore_ascii_case(CHECKED_AS_GLOBAL_RULE.0)
39                && label == CHECKED_AS_GLOBAL_RULE.1
40            {
41                continue;
42            }
43            // `SELF\X.Y` names an inherited attribute: the slot is `Y`'s.
44            let names: Vec<&str> = rule
45                .attributes
46                .iter()
47                .map(|name| name.rsplit(['.', '\\']).next().unwrap_or(name).trim())
48                .collect();
49            check(model, schema, &declaring.name, label, &names, report);
50        }
51    }
52}
53
54fn check(
55    model: &Model,
56    schema: &Schema,
57    declaring: &str,
58    label: &str,
59    names: &[&str],
60    report: &mut Report,
61) {
62    let mut instances: Vec<EntityId> = Vec::new();
63    for (type_name, _) in model.type_histogram() {
64        if schema.is_a(type_name, declaring) {
65            instances.extend_from_slice(model.ids_of_type(type_name));
66        }
67    }
68    instances.sort_unstable();
69    let mut seen: HashMap<String, EntityId> = HashMap::new();
70    for id in instances {
71        let Some(entity) = model.get(id) else {
72            continue;
73        };
74        let layout = schema.attribute_names(&entity.type_name);
75        let slots: Option<Vec<usize>> = names
76            .iter()
77            .map(|name| {
78                layout
79                    .iter()
80                    .position(|slot| slot.eq_ignore_ascii_case(name))
81            })
82            .collect();
83        let Some(slots) = slots else {
84            continue;
85        };
86        let values: Option<Vec<&Value>> = slots
87            .iter()
88            .map(|&slot| {
89                entity
90                    .attributes
91                    .get(slot)
92                    .filter(|value| !matches!(value, Value::Null | Value::Derived))
93            })
94            .collect();
95        let Some(values) = values else {
96            continue;
97        };
98        let key = format!("{values:?}");
99        match seen.get(&key) {
100            Some(first) => report.push(Finding::error(
101                "structure.unique.violation",
102                Path::Attribute {
103                    entity: id,
104                    index: slots[0],
105                    name: Some(names[0].to_owned()),
106                },
107                format!(
108                    "{declaring}.{label}: {} must be unique across {declaring}, \
109                     and {first} already holds the same value",
110                    names.join(", ")
111                ),
112            )),
113            None => {
114                seen.insert(key, id);
115            }
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use ifc_model::Entity;
124    use std::sync::Arc;
125
126    fn application(schema: &Schema, identifier: &str, name: &str, version: &str) -> Entity {
127        let names = schema.attribute_names("IfcApplication");
128        let mut values = vec![Value::Null; names.len()];
129        let mut set = |attribute: &str, value: Value| {
130            let slot = names.iter().position(|n| *n == attribute).unwrap();
131            values[slot] = value;
132        };
133        set("ApplicationIdentifier", Value::Text(Arc::from(identifier)));
134        set("ApplicationFullName", Value::Text(Arc::from(name)));
135        set("Version", Value::Text(Arc::from(version)));
136        Entity::new("IFCAPPLICATION", values)
137    }
138
139    fn run(entities: Vec<Entity>) -> Vec<String> {
140        let schema = ifc_schema::ifc4();
141        let mut model = Model::new();
142        for entity in entities {
143            model.push(entity);
144        }
145        let mut report = Report::new();
146        unique_rules(&model, schema, &mut report);
147        report
148            .findings()
149            .iter()
150            .map(|f| f.message.clone())
151            .collect()
152    }
153
154    #[test]
155    fn a_repeated_clause_is_reported_once_per_repeat() {
156        let schema = ifc_schema::ifc4();
157        assert!(run(vec![
158            application(schema, "a", "App", "1"),
159            application(schema, "b", "App", "2"),
160        ])
161        .is_empty());
162        let found = run(vec![
163            application(schema, "a", "App", "1"),
164            application(schema, "a", "Other", "1"),
165        ]);
166        assert_eq!(found.len(), 1, "{found:?}");
167        assert!(found[0].starts_with("IfcApplication.UR1"), "{found:?}");
168        let found = run(vec![
169            application(schema, "a", "App", "1"),
170            application(schema, "b", "App", "1"),
171        ]);
172        assert_eq!(found.len(), 1, "{found:?}");
173        assert!(found[0].starts_with("IfcApplication.UR2"), "{found:?}");
174    }
175}