ifc_validate/structure/
unique.rs1use std::collections::HashMap;
21
22use ifc_model::{EntityId, Model, Value};
23use ifc_schema::Schema;
24
25use crate::report::{Finding, Path, Report};
26
27const CHECKED_AS_GLOBAL_RULE: (&str, &str) = ("IfcRoot", "UR1");
29
30pub 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 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}