Skip to main content

ifc_constraint/
projection.rs

1//! Strict borrowed constraint projections and direct relationship queries.
2
3use ifc_model::guid::Guid;
4use ifc_model::{Entity, EntityId, Value};
5
6use crate::types::{Benchmark, ConstraintGrade, LogicalOperator, MetricValue, ObjectiveQualifier};
7use crate::view::{
8    invalid, optional_ref, optional_text, required_ref, required_refs, required_text,
9    validate_target, wrong, ConstraintView,
10};
11use crate::{ConstraintError, ConstraintResult};
12
13const METRIC: &str = "IFCMETRIC";
14const OBJECTIVE: &str = "IFCOBJECTIVE";
15const RESOURCE_REL: &str = "IFCRESOURCECONSTRAINTRELATIONSHIP";
16const ASSIGNMENT: &str = "IFCRELASSOCIATESCONSTRAINT";
17
18macro_rules! projection {
19    ($name:ident, $kind:expr) => {
20        /// Strict borrowed IFC4 projection.
21        #[derive(Debug, Clone, Copy)]
22        pub struct $name<'m> {
23            id: EntityId,
24            entity: &'m Entity,
25        }
26        impl<'m> $name<'m> {
27            /// Construct from an entity of the exact expected kind.
28            pub fn try_new(id: EntityId, entity: &'m Entity) -> ConstraintResult<Self> {
29                if entity.is_type($kind) {
30                    Ok(Self { id, entity })
31                } else {
32                    Err(wrong($kind, entity))
33                }
34            }
35            /// Stable model identifier.
36            #[must_use]
37            pub const fn id(self) -> EntityId {
38                self.id
39            }
40        }
41    };
42}
43
44projection!(Metric, METRIC);
45projection!(Objective, OBJECTIVE);
46projection!(ResourceConstraintRelationship, RESOURCE_REL);
47projection!(ConstraintAssignment, ASSIGNMENT);
48
49fn grade(kind: &'static str, id: EntityId, entity: &Entity) -> ConstraintResult<ConstraintGrade> {
50    match entity.attribute(2) {
51        Some(Value::Enum(value)) => ConstraintGrade::parse(value)
52            .ok_or_else(|| invalid(kind, id, "ConstraintGrade", &Value::Enum(value.clone()))),
53        Some(value) => Err(invalid(kind, id, "ConstraintGrade", value)),
54        None => Err(ConstraintError::MissingAttribute {
55            entity: kind,
56            id,
57            attribute: "ConstraintGrade",
58        }),
59    }
60}
61
62fn validate_base(
63    view: ConstraintView<'_>,
64    kind: &'static str,
65    id: EntityId,
66    entity: &Entity,
67) -> ConstraintResult<()> {
68    required_text(kind, id, entity, 0, "Name")?;
69    let grade = grade(kind, id, entity)?;
70    if grade == ConstraintGrade::UserDefined
71        && optional_text(kind, id, entity, 6, "UserDefinedGrade")?.is_none()
72    {
73        return Err(ConstraintError::Semantic {
74            entity: kind,
75            id,
76            rule: "WR11",
77            detail: "USERDEFINED grade requires UserDefinedGrade".into(),
78        });
79    }
80    if let Some(actor) = optional_ref(kind, id, entity, 4, "CreatingActor")? {
81        validate_target(
82            view.model(),
83            kind,
84            id,
85            "CreatingActor",
86            actor,
87            "IfcActorSelect",
88        )?;
89    }
90    Ok(())
91}
92
93macro_rules! base_accessors {
94    ($name:ident, $kind:expr) => {
95        impl<'m> $name<'m> {
96            /// Constraint name.
97            pub fn name(self) -> ConstraintResult<&'m str> {
98                required_text($kind, self.id, self.entity, 0, "Name")
99            }
100            /// Optional description.
101            pub fn description(self) -> ConstraintResult<Option<&'m str>> {
102                optional_text($kind, self.id, self.entity, 1, "Description")
103            }
104            /// Typed constraint grade.
105            pub fn grade(self) -> ConstraintResult<ConstraintGrade> {
106                grade($kind, self.id, self.entity)
107            }
108            /// Optional source label.
109            pub fn source(self) -> ConstraintResult<Option<&'m str>> {
110                optional_text($kind, self.id, self.entity, 3, "ConstraintSource")
111            }
112            /// Optional creating actor.
113            pub fn creating_actor(self) -> ConstraintResult<Option<EntityId>> {
114                optional_ref($kind, self.id, self.entity, 4, "CreatingActor")
115            }
116            /// Optional creation time lexical value.
117            pub fn creation_time(self) -> ConstraintResult<Option<&'m str>> {
118                optional_text($kind, self.id, self.entity, 5, "CreationTime")
119            }
120            /// Optional user-defined grade.
121            pub fn user_defined_grade(self) -> ConstraintResult<Option<&'m str>> {
122                optional_text($kind, self.id, self.entity, 6, "UserDefinedGrade")
123            }
124        }
125    };
126}
127
128base_accessors!(Metric, METRIC);
129base_accessors!(Objective, OBJECTIVE);
130
131impl<'m> Metric<'m> {
132    /// Typed comparison benchmark.
133    pub fn benchmark(self) -> ConstraintResult<Benchmark> {
134        match self.entity.attribute(7) {
135            Some(Value::Enum(value)) => Benchmark::parse(value)
136                .ok_or_else(|| invalid(METRIC, self.id, "Benchmark", &Value::Enum(value.clone()))),
137            Some(value) => Err(invalid(METRIC, self.id, "Benchmark", value)),
138            None => Err(ConstraintError::MissingAttribute {
139                entity: METRIC,
140                id: self.id,
141                attribute: "Benchmark",
142            }),
143        }
144    }
145
146    /// Optional source label for the metric value.
147    pub fn value_source(self) -> ConstraintResult<Option<&'m str>> {
148        optional_text(METRIC, self.id, self.entity, 8, "ValueSource")
149    }
150
151    /// Preserved optional `IfcMetricValueSelect`.
152    pub fn data_value(self) -> ConstraintResult<Option<MetricValue<'m>>> {
153        match self.entity.attribute(9) {
154            None | Some(Value::Null) => Ok(None),
155            Some(Value::Ref(target)) => Ok(Some(MetricValue::Entity(*target))),
156            Some(Value::Typed { type_name, value })
157                if ifc_schema::ifc4().accepts_type("IfcMetricValueSelect", type_name) =>
158            {
159                Ok(Some(MetricValue::Typed {
160                    type_name,
161                    value: value.as_ref(),
162                }))
163            }
164            Some(value) => Err(invalid(METRIC, self.id, "DataValue", value)),
165        }
166    }
167
168    /// Optional `IfcReference` path.
169    pub fn reference_path(self) -> ConstraintResult<Option<EntityId>> {
170        optional_ref(METRIC, self.id, self.entity, 10, "ReferencePath")
171    }
172
173    fn validate(self, view: ConstraintView<'m>) -> ConstraintResult<Self> {
174        validate_base(view, METRIC, self.id, self.entity)?;
175        self.benchmark()?;
176        match self.entity.attribute(9) {
177            None | Some(Value::Null) => {}
178            Some(Value::Ref(target)) => validate_target(
179                view.model(),
180                METRIC,
181                self.id,
182                "DataValue",
183                *target,
184                "IfcMetricValueSelect",
185            )?,
186            Some(Value::Typed { type_name, .. })
187                if ifc_schema::ifc4().accepts_type("IfcMetricValueSelect", type_name) => {}
188            Some(value) => return Err(invalid(METRIC, self.id, "DataValue", value)),
189        }
190        if let Some(reference) = self.reference_path()? {
191            validate_target(
192                view.model(),
193                METRIC,
194                self.id,
195                "ReferencePath",
196                reference,
197                "IfcReference",
198            )?;
199        }
200        Ok(self)
201    }
202}
203
204impl<'m> Objective<'m> {
205    /// Optional ordered benchmark constraints.
206    ///
207    /// `BenchmarkValues` is a `LIST`, not a `SET`: the authored order is
208    /// significant and returned unchanged, never sorted or deduplicated.
209    pub fn benchmark_values(self) -> ConstraintResult<Option<Vec<EntityId>>> {
210        match self.entity.attribute(7) {
211            None | Some(Value::Null) => Ok(None),
212            Some(Value::List(values)) if !values.is_empty() => values
213                .iter()
214                .map(|value| match value {
215                    Value::Ref(target) => Ok(*target),
216                    other => Err(invalid(OBJECTIVE, self.id, "BenchmarkValues", other)),
217                })
218                .collect::<ConstraintResult<Vec<_>>>()
219                .map(Some),
220            Some(value) => Err(invalid(OBJECTIVE, self.id, "BenchmarkValues", value)),
221        }
222    }
223
224    /// Optional typed logical aggregator.
225    pub fn logical_aggregator(self) -> ConstraintResult<Option<LogicalOperator>> {
226        match self.entity.attribute(8) {
227            None | Some(Value::Null) => Ok(None),
228            Some(Value::Enum(value)) => LogicalOperator::parse(value).map(Some).ok_or_else(|| {
229                invalid(
230                    OBJECTIVE,
231                    self.id,
232                    "LogicalAggregator",
233                    &Value::Enum(value.clone()),
234                )
235            }),
236            Some(value) => Err(invalid(OBJECTIVE, self.id, "LogicalAggregator", value)),
237        }
238    }
239
240    /// Typed objective qualifier.
241    pub fn qualifier(self) -> ConstraintResult<ObjectiveQualifier> {
242        match self.entity.attribute(9) {
243            Some(Value::Enum(value)) => ObjectiveQualifier::parse(value).ok_or_else(|| {
244                invalid(
245                    OBJECTIVE,
246                    self.id,
247                    "ObjectiveQualifier",
248                    &Value::Enum(value.clone()),
249                )
250            }),
251            Some(value) => Err(invalid(OBJECTIVE, self.id, "ObjectiveQualifier", value)),
252            None => Err(ConstraintError::MissingAttribute {
253                entity: OBJECTIVE,
254                id: self.id,
255                attribute: "ObjectiveQualifier",
256            }),
257        }
258    }
259
260    /// Optional user-defined objective qualifier.
261    pub fn user_defined_qualifier(self) -> ConstraintResult<Option<&'m str>> {
262        optional_text(OBJECTIVE, self.id, self.entity, 10, "UserDefinedQualifier")
263    }
264
265    fn validate(self, view: ConstraintView<'m>) -> ConstraintResult<Self> {
266        validate_base(view, OBJECTIVE, self.id, self.entity)?;
267        self.logical_aggregator()?;
268        if self.qualifier()? == ObjectiveQualifier::UserDefined
269            && self.user_defined_qualifier()?.is_none()
270        {
271            return Err(ConstraintError::Semantic {
272                entity: OBJECTIVE,
273                id: self.id,
274                rule: "WR21",
275                detail: "USERDEFINED qualifier requires UserDefinedQualifier".into(),
276            });
277        }
278        if let Some(values) = self.benchmark_values()? {
279            for target in values {
280                validate_target(
281                    view.model(),
282                    OBJECTIVE,
283                    self.id,
284                    "BenchmarkValues",
285                    target,
286                    "IfcConstraint",
287                )?;
288            }
289        }
290        Ok(self)
291    }
292}
293
294impl<'m> ResourceConstraintRelationship<'m> {
295    /// Optional relationship name.
296    pub fn name(self) -> ConstraintResult<Option<&'m str>> {
297        optional_text(RESOURCE_REL, self.id, self.entity, 0, "Name")
298    }
299    /// Optional relationship description.
300    pub fn description(self) -> ConstraintResult<Option<&'m str>> {
301        optional_text(RESOURCE_REL, self.id, self.entity, 1, "Description")
302    }
303    /// Relating metric or objective.
304    pub fn relating_constraint(self) -> ConstraintResult<EntityId> {
305        required_ref(RESOURCE_REL, self.id, self.entity, 2, "RelatingConstraint")
306    }
307    /// Non-empty unique resource-select targets.
308    pub fn related_resources(self) -> ConstraintResult<Vec<EntityId>> {
309        required_refs(
310            RESOURCE_REL,
311            self.id,
312            self.entity,
313            3,
314            "RelatedResourceObjects",
315        )
316    }
317    fn validate(self, view: ConstraintView<'m>) -> ConstraintResult<Self> {
318        validate_target(
319            view.model(),
320            RESOURCE_REL,
321            self.id,
322            "RelatingConstraint",
323            self.relating_constraint()?,
324            "IfcConstraint",
325        )?;
326        for target in self.related_resources()? {
327            validate_target(
328                view.model(),
329                RESOURCE_REL,
330                self.id,
331                "RelatedResourceObjects",
332                target,
333                "IfcResourceObjectSelect",
334            )?;
335        }
336        Ok(self)
337    }
338}
339
340impl<'m> ConstraintAssignment<'m> {
341    /// Root GlobalId.
342    pub fn global_id(self) -> ConstraintResult<&'m str> {
343        required_text(ASSIGNMENT, self.id, self.entity, 0, "GlobalId")
344    }
345    /// Optional association intent.
346    pub fn intent(self) -> ConstraintResult<Option<&'m str>> {
347        optional_text(ASSIGNMENT, self.id, self.entity, 5, "Intent")
348    }
349    /// Related definition-select targets.
350    pub fn related_objects(self) -> ConstraintResult<Vec<EntityId>> {
351        required_refs(ASSIGNMENT, self.id, self.entity, 4, "RelatedObjects")
352    }
353    /// Relating metric or objective.
354    pub fn relating_constraint(self) -> ConstraintResult<EntityId> {
355        required_ref(ASSIGNMENT, self.id, self.entity, 6, "RelatingConstraint")
356    }
357    fn validate(self, view: ConstraintView<'m>) -> ConstraintResult<Self> {
358        if Guid::parse(self.global_id()?).is_none() {
359            return Err(ConstraintError::InvalidValue {
360                entity: ASSIGNMENT,
361                id: self.id,
362                attribute: "GlobalId",
363                value: self.global_id()?.into(),
364            });
365        }
366        validate_target(
367            view.model(),
368            ASSIGNMENT,
369            self.id,
370            "RelatingConstraint",
371            self.relating_constraint()?,
372            "IfcConstraint",
373        )?;
374        for target in self.related_objects()? {
375            validate_target(
376                view.model(),
377                ASSIGNMENT,
378                self.id,
379                "RelatedObjects",
380                target,
381                "IfcDefinitionSelect",
382            )?;
383        }
384        Ok(self)
385    }
386}
387
388impl<'m> ConstraintView<'m> {
389    /// Strictly project one metric.
390    pub fn metric(self, id: EntityId) -> ConstraintResult<Metric<'m>> {
391        let entity = self
392            .model()
393            .get(id)
394            .ok_or(ConstraintError::UnknownEntity { id })?;
395        Metric::try_new(id, entity)?.validate(self)
396    }
397    /// Strictly project one objective.
398    pub fn objective(self, id: EntityId) -> ConstraintResult<Objective<'m>> {
399        let entity = self
400            .model()
401            .get(id)
402            .ok_or(ConstraintError::UnknownEntity { id })?;
403        Objective::try_new(id, entity)?.validate(self)
404    }
405    /// Strictly project one resource constraint relationship.
406    pub fn resource_constraint_relationship(
407        self,
408        id: EntityId,
409    ) -> ConstraintResult<ResourceConstraintRelationship<'m>> {
410        let entity = self
411            .model()
412            .get(id)
413            .ok_or(ConstraintError::UnknownEntity { id })?;
414        ResourceConstraintRelationship::try_new(id, entity)?.validate(self)
415    }
416    /// Strictly project one rooted constraint association.
417    pub fn constraint_assignment(self, id: EntityId) -> ConstraintResult<ConstraintAssignment<'m>> {
418        let entity = self
419            .model()
420            .get(id)
421            .ok_or(ConstraintError::UnknownEntity { id })?;
422        ConstraintAssignment::try_new(id, entity)?.validate(self)
423    }
424    /// Resource-select IDs directly governed by a constraint.
425    pub fn resources_constrained_by(self, constraint: EntityId) -> ConstraintResult<Vec<EntityId>> {
426        self.constraint(constraint)?;
427        let mut out = Vec::new();
428        for (id, entity) in self.model().of_type(RESOURCE_REL) {
429            let relationship = ResourceConstraintRelationship { id, entity };
430            if relationship.relating_constraint()? == constraint {
431                out.extend(relationship.validate(self)?.related_resources()?);
432            }
433        }
434        Ok(out)
435    }
436    /// Definition-select IDs directly associated with a constraint.
437    pub fn objects_constrained_by(self, constraint: EntityId) -> ConstraintResult<Vec<EntityId>> {
438        self.constraint(constraint)?;
439        let mut out = Vec::new();
440        for (id, entity) in self.model().of_type(ASSIGNMENT) {
441            let relationship = ConstraintAssignment { id, entity };
442            if relationship.relating_constraint()? == constraint {
443                out.extend(relationship.validate(self)?.related_objects()?);
444            }
445        }
446        Ok(out)
447    }
448    fn constraint(self, id: EntityId) -> ConstraintResult<()> {
449        let entity = self
450            .model()
451            .get(id)
452            .ok_or(ConstraintError::UnknownEntity { id })?;
453        if entity.is_type(METRIC) {
454            self.metric(id).map(|_| ())
455        } else if entity.is_type(OBJECTIVE) {
456            self.objective(id).map(|_| ())
457        } else {
458            Err(wrong("IfcConstraint", entity))
459        }
460    }
461}