Skip to main content

ifc_constraint/
projection.rs

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