1use std::collections::HashSet;
6use std::sync::Arc;
7
8use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
9use ifc_schema::Schema;
10
11use crate::types::{
12 Benchmark, ConstraintGrade, LogicalOperator, MetricValueDraft, ObjectiveQualifier,
13};
14use crate::{ConstraintError, ConstraintResult};
15
16const METRIC: &str = "IFCMETRIC";
17const OBJECTIVE: &str = "IFCOBJECTIVE";
18const RESOURCE_REL: &str = "IFCRESOURCECONSTRAINTRELATIONSHIP";
19
20#[derive(Debug, Clone, Copy)]
22pub struct ConstraintBaseDraft<'a> {
23 pub name: &'a str,
25 pub description: Option<&'a str>,
27 pub grade: ConstraintGrade,
29 pub source: Option<&'a str>,
31 pub creating_actor: Option<EntityId>,
33 pub creation_time: Option<&'a str>,
35 pub user_defined_grade: Option<&'a str>,
37}
38
39#[derive(Debug, Clone, Copy)]
41pub struct MetricDraft<'a> {
42 pub base: ConstraintBaseDraft<'a>,
44 pub benchmark: Benchmark,
46 pub value_source: Option<&'a str>,
48 pub data_value: Option<MetricValueDraft<'a>>,
50 pub reference_path: Option<EntityId>,
52}
53
54#[derive(Debug, Clone, Copy)]
56pub struct ObjectiveDraft<'a> {
57 pub base: ConstraintBaseDraft<'a>,
59 pub benchmark_values: Option<&'a [EntityId]>,
61 pub logical_aggregator: Option<LogicalOperator>,
63 pub qualifier: ObjectiveQualifier,
65 pub user_defined_qualifier: Option<&'a str>,
67}
68
69#[derive(Debug, Clone, Copy)]
71pub struct ResourceConstraintDraft<'a> {
72 pub name: Option<&'a str>,
74 pub description: Option<&'a str>,
76 pub relating_constraint: EntityId,
78 pub related_resources: &'a [EntityId],
80}
81
82pub fn create_metric(
84 tx: &mut Transaction,
85 model: &Model,
86 draft: MetricDraft<'_>,
87) -> ConstraintResult<EntityId> {
88 validate_base(tx, model, METRIC, draft.base)?;
89 if let Some(path) = draft.reference_path {
90 validate_target(tx, model, path, "IfcReference")?;
91 }
92 let data_value = match draft.data_value {
93 None => Value::Null,
94 Some(MetricValueDraft::Entity(target)) => {
95 validate_target(tx, model, target, "IfcMetricValueSelect")?;
96 Value::Ref(target)
97 }
98 Some(MetricValueDraft::Typed { type_name, value }) => {
99 if !ifc_schema::ifc4().accepts_type("IfcMetricValueSelect", type_name) {
100 return Err(ConstraintError::AuthoringInvalid {
101 entity: METRIC,
102 attribute: "DataValue",
103 value: format!("type {type_name} is outside IfcMetricValueSelect"),
104 });
105 }
106 Value::Typed {
107 type_name: Arc::from(type_name.to_ascii_uppercase()),
108 value: Box::new(value.clone()),
109 }
110 }
111 };
112 Ok(tx.create(Entity::new(
113 METRIC,
114 vec![
115 text(draft.base.name),
116 optional_text(draft.base.description),
117 enumeration(draft.base.grade.token()),
118 optional_text(draft.base.source),
119 optional_ref(draft.base.creating_actor),
120 optional_text(draft.base.creation_time),
121 optional_text(draft.base.user_defined_grade),
122 enumeration(draft.benchmark.token()),
123 optional_text(draft.value_source),
124 data_value,
125 optional_ref(draft.reference_path),
126 ],
127 )))
128}
129
130pub fn create_objective(
132 tx: &mut Transaction,
133 model: &Model,
134 draft: ObjectiveDraft<'_>,
135) -> ConstraintResult<EntityId> {
136 validate_base(tx, model, OBJECTIVE, draft.base)?;
137 if draft.qualifier == ObjectiveQualifier::UserDefined && draft.user_defined_qualifier.is_none()
138 {
139 return Err(ConstraintError::AuthoringInvalid {
140 entity: OBJECTIVE,
141 attribute: "WR21",
142 value: "USERDEFINED qualifier requires UserDefinedQualifier".into(),
143 });
144 }
145 let benchmarks = match draft.benchmark_values {
146 None => Value::Null,
147 Some([]) => {
148 return Err(ConstraintError::AuthoringInvalid {
149 entity: OBJECTIVE,
150 attribute: "BenchmarkValues",
151 value: "empty LIST [1:?]".into(),
152 });
153 }
154 Some(values) => {
155 for &target in values {
156 validate_target(tx, model, target, "IfcConstraint")?;
157 }
158 refs(values)
159 }
160 };
161 Ok(tx.create(Entity::new(
162 OBJECTIVE,
163 vec![
164 text(draft.base.name),
165 optional_text(draft.base.description),
166 enumeration(draft.base.grade.token()),
167 optional_text(draft.base.source),
168 optional_ref(draft.base.creating_actor),
169 optional_text(draft.base.creation_time),
170 optional_text(draft.base.user_defined_grade),
171 benchmarks,
172 draft
173 .logical_aggregator
174 .map_or(Value::Null, |value| enumeration(value.token())),
175 enumeration(draft.qualifier.token()),
176 optional_text(draft.user_defined_qualifier),
177 ],
178 )))
179}
180
181pub fn relate_resource_constraint(
183 tx: &mut Transaction,
184 model: &Model,
185 draft: ResourceConstraintDraft<'_>,
186) -> ConstraintResult<EntityId> {
187 validate_target(tx, model, draft.relating_constraint, "IfcConstraint")?;
188 validate_set(
189 tx,
190 model,
191 RESOURCE_REL,
192 "RelatedResourceObjects",
193 draft.related_resources,
194 "IfcResourceObjectSelect",
195 )?;
196 Ok(tx.create(Entity::new(
197 RESOURCE_REL,
198 vec![
199 optional_text(draft.name),
200 optional_text(draft.description),
201 Value::Ref(draft.relating_constraint),
202 refs(draft.related_resources),
203 ],
204 )))
205}
206
207fn validate_base(
208 tx: &Transaction,
209 model: &Model,
210 kind: &'static str,
211 draft: ConstraintBaseDraft<'_>,
212) -> ConstraintResult<()> {
213 if draft.grade == ConstraintGrade::UserDefined && draft.user_defined_grade.is_none() {
214 return Err(ConstraintError::AuthoringInvalid {
215 entity: kind,
216 attribute: "WR11",
217 value: "USERDEFINED grade requires UserDefinedGrade".into(),
218 });
219 }
220 if let Some(actor) = draft.creating_actor {
221 validate_target(tx, model, actor, "IfcActorSelect")?;
222 }
223 Ok(())
224}
225
226fn validate_set(
227 tx: &Transaction,
228 model: &Model,
229 kind: &'static str,
230 attribute: &'static str,
231 targets: &[EntityId],
232 expected: &'static str,
233) -> ConstraintResult<()> {
234 if targets.is_empty() {
235 return Err(ConstraintError::AuthoringInvalid {
236 entity: kind,
237 attribute,
238 value: "empty SET [1:?]".into(),
239 });
240 }
241 let mut seen = HashSet::new();
242 for &target in targets {
243 if !seen.insert(target) {
244 return Err(ConstraintError::AuthoringInvalid {
245 entity: kind,
246 attribute,
247 value: format!("duplicate {target}"),
248 });
249 }
250 validate_target(tx, model, target, expected)?;
251 }
252 Ok(())
253}
254
255#[derive(Debug, Clone, Copy)]
257pub struct ReferenceDraft<'a> {
258 pub type_identifier: Option<&'a str>,
260 pub attribute_identifier: Option<&'a str>,
262 pub instance_name: Option<&'a str>,
264 pub list_positions: &'a [i64],
266 pub inner_reference: Option<EntityId>,
268}
269
270pub fn create_reference(
287 tx: &mut Transaction,
288 model: &Model,
289 draft: ReferenceDraft<'_>,
290) -> ConstraintResult<EntityId> {
291 const ENTITY: &str = "IfcReference";
292 let empty = draft.type_identifier.is_none()
293 && draft.attribute_identifier.is_none()
294 && draft.instance_name.is_none()
295 && draft.list_positions.is_empty()
296 && draft.inner_reference.is_none();
297 if empty {
298 return Err(ConstraintError::AuthoringInvalid {
299 entity: ENTITY,
300 attribute: "TypeIdentifier",
301 value: "a reference with every slot unset addresses nothing".to_owned(),
302 });
303 }
304 for position in draft.list_positions {
305 if *position < 1 {
306 return Err(ConstraintError::AuthoringInvalid {
307 entity: ENTITY,
308 attribute: "ListPositions",
309 value: format!("{position} is not a 1-based list index"),
310 });
311 }
312 }
313 if let Some(inner) = draft.inner_reference {
314 validate_target(tx, model, inner, ENTITY)?;
315 }
316
317 let positions = if draft.list_positions.is_empty() {
318 Value::Null
321 } else {
322 Value::List(
323 draft
324 .list_positions
325 .iter()
326 .copied()
327 .map(Value::Integer)
328 .collect(),
329 )
330 };
331 Ok(tx.create(Entity::new(
332 "IFCREFERENCE",
333 vec![
334 optional_text(draft.type_identifier),
335 optional_text(draft.attribute_identifier),
336 optional_text(draft.instance_name),
337 positions,
338 draft.inner_reference.map_or(Value::Null, Value::Ref),
339 ],
340 )))
341}
342
343fn validate_target(
344 tx: &Transaction,
345 model: &Model,
346 target: EntityId,
347 expected: &'static str,
348) -> ConstraintResult<()> {
349 validate_target_in(ifc_schema::ifc4(), tx, model, target, expected)
350}
351
352pub(crate) fn validate_target_in(
354 schema: &Schema,
355 tx: &Transaction,
356 model: &Model,
357 target: EntityId,
358 expected: &'static str,
359) -> ConstraintResult<()> {
360 let actual =
361 final_type(tx, model, target).ok_or(ConstraintError::UnknownEntity { id: target })?;
362 if schema.accepts_type(expected, actual) {
363 Ok(())
364 } else {
365 Err(ConstraintError::AuthoringReferenceType {
366 target,
367 expected,
368 actual: actual.into(),
369 })
370 }
371}
372
373pub(crate) fn final_type<'a>(
374 tx: &'a Transaction,
375 model: &'a Model,
376 id: EntityId,
377) -> Option<&'a str> {
378 for edit in tx.edits().iter().rev() {
379 match edit {
380 Edit::Create {
381 id: edit_id,
382 entity,
383 } if *edit_id == id => return Some(&entity.type_name),
384 Edit::Remove { id: edit_id } if *edit_id == id => return None,
385 Edit::Retype {
386 id: edit_id,
387 type_name,
388 } if *edit_id == id => return Some(type_name),
389 _ => {}
390 }
391 }
392 model.get(id).map(|entity| entity.type_name.as_ref())
393}
394
395pub(crate) fn text(value: &str) -> Value {
396 Value::Text(Arc::from(value))
397}
398pub(crate) fn optional_text(value: Option<&str>) -> Value {
399 value.map_or(Value::Null, text)
400}
401pub(crate) fn optional_ref(value: Option<EntityId>) -> Value {
402 value.map_or(Value::Null, Value::Ref)
403}
404fn enumeration(value: &str) -> Value {
405 Value::Enum(Arc::from(value))
406}
407pub(crate) fn refs(values: &[EntityId]) -> Value {
408 Value::List(values.iter().copied().map(Value::Ref).collect())
409}