Skip to main content

ifc_approval/
projection.rs

1//! Strict borrowed projections and deterministic direct queries, read by
2//! attribute name in the model's declared release (#212).
3
4use ifc_model::guid::Guid;
5use ifc_model::{Entity, EntityId};
6use ifc_schema::SchemaVersion;
7
8use crate::release::Layout;
9use crate::view::{wrong, ApprovalView, Record};
10use crate::{ApprovalError, ApprovalResult};
11
12const APPROVAL: &str = "IFCAPPROVAL";
13const APPROVAL_REL: &str = "IFCAPPROVALRELATIONSHIP";
14const RESOURCE_REL: &str = "IFCRESOURCEAPPROVALRELATIONSHIP";
15const ASSIGNMENT: &str = "IFCRELASSOCIATESAPPROVAL";
16
17macro_rules! projection {
18    ($name:ident, $kind:expr) => {
19        /// Strict borrowed projection, read against one release.
20        #[derive(Debug, Clone, Copy)]
21        pub struct $name<'m> {
22            record: Record<'m>,
23        }
24        impl<'m> $name<'m> {
25            /// Construct from an entity of the exact expected kind, read
26            /// against `release`.
27            ///
28            /// [`ApprovalView`] binds the model's declared release; use this
29            /// when the release is known some other way.
30            ///
31            /// # Errors
32            ///
33            /// `WrongEntityType` for another entity, `UnsupportedSchema`
34            /// for a release this crate is not verified against (IFC4X1,
35            /// IFC4X2), and `EntityNotInSchema` for an entity the release
36            /// does not declare.
37            pub fn try_new(
38                id: EntityId,
39                entity: &'m Entity,
40                release: SchemaVersion,
41            ) -> ApprovalResult<Self> {
42                Self::bound(id, entity, Layout::of_version(release)?)
43            }
44            pub(crate) fn bound(
45                id: EntityId,
46                entity: &'m Entity,
47                layout: Layout,
48            ) -> ApprovalResult<Self> {
49                if !entity.is_type($kind) {
50                    return Err(wrong($kind, entity));
51                }
52                layout.require_entity($kind)?;
53                Ok(Self {
54                    record: Record {
55                        kind: $kind,
56                        id,
57                        entity,
58                        layout,
59                    },
60                })
61            }
62            /// Stable model identifier.
63            #[must_use]
64            pub const fn id(self) -> EntityId {
65                self.record.id
66            }
67            /// The release this projection reads against.
68            #[must_use]
69            pub const fn release(self) -> SchemaVersion {
70                self.record.layout.version()
71            }
72        }
73    };
74}
75
76projection!(Approval, APPROVAL);
77projection!(ApprovalRelationship, APPROVAL_REL);
78projection!(ResourceApprovalRelationship, RESOURCE_REL);
79projection!(ApprovalAssignment, ASSIGNMENT);
80
81/// Fail with `MissingAttribute` when the release requires `attribute` and
82/// the record leaves it `$`.
83fn required_if_declared(record: Record<'_>, attribute: &'static str) -> ApprovalResult<()> {
84    if record.requires(attribute) {
85        record.required_text(attribute)?;
86    }
87    Ok(())
88}
89
90impl<'m> Approval<'m> {
91    /// Optional approval identifier (required in IFC2X3).
92    pub fn identifier(self) -> ApprovalResult<Option<&'m str>> {
93        self.record.optional_text("Identifier")
94    }
95    /// Optional approval name (required in IFC2X3).
96    pub fn name(self) -> ApprovalResult<Option<&'m str>> {
97        self.record.optional_text("Name")
98    }
99    /// Optional description.
100    pub fn description(self) -> ApprovalResult<Option<&'m str>> {
101        self.record.optional_text("Description")
102    }
103    /// Optional authored approval time, IFC4/IFC4X3 `IfcDateTime` text.
104    ///
105    /// IFC2X3's `ApprovalDateTime` (which IFC4 renamed to it) is an
106    /// `IfcDateTimeSelect` record: that is `StructuredValue` with the
107    /// record's id, never read as text.
108    pub fn time_of_approval(self) -> ApprovalResult<Option<&'m str>> {
109        self.record.optional_text("TimeOfApproval")
110    }
111    /// Optional status label. `NotInSchema` in IFC2X3.
112    pub fn status(self) -> ApprovalResult<Option<&'m str>> {
113        self.record.optional_text("Status")
114    }
115    /// Optional approval level label. `NotInSchema` in IFC2X3.
116    pub fn level(self) -> ApprovalResult<Option<&'m str>> {
117        self.record.optional_text("Level")
118    }
119    /// Optional qualifier text. `NotInSchema` in IFC2X3.
120    pub fn qualifier(self) -> ApprovalResult<Option<&'m str>> {
121        self.record.optional_text("Qualifier")
122    }
123    /// Optional requesting actor-select reference. `NotInSchema` in IFC2X3,
124    /// which relates actors through `IfcApprovalActorRelationship`.
125    pub fn requesting_approval(self) -> ApprovalResult<Option<EntityId>> {
126        self.record.optional_ref("RequestingApproval")
127    }
128    /// Optional giving actor-select reference. `NotInSchema` in IFC2X3.
129    pub fn giving_approval(self) -> ApprovalResult<Option<EntityId>> {
130        self.record.optional_ref("GivingApproval")
131    }
132
133    fn validate(self, view: ApprovalView<'m>) -> ApprovalResult<Self> {
134        let record = self.record;
135        required_if_declared(record, "Identifier")?;
136        required_if_declared(record, "Name")?;
137        if self.identifier()?.is_none() && self.name()?.is_none() {
138            return Err(ApprovalError::Semantic {
139                entity: APPROVAL,
140                id: record.id,
141                rule: "HasIdentifierOrName",
142                detail: "Identifier and Name are both absent".into(),
143            });
144        }
145        match self.time_of_approval() {
146            Ok(_) => {}
147            Err(ApprovalError::StructuredValue { target, .. }) => record.validate_target(
148                view.model(),
149                "TimeOfApproval",
150                target,
151                "IfcDateTimeSelect",
152            )?,
153            Err(error) => return Err(error),
154        }
155        // IFC2X3 requires ApprovalDateTime; a `$` is a missing value.
156        if record.requires("TimeOfApproval") && matches!(self.time_of_approval(), Ok(None)) {
157            return Err(ApprovalError::MissingAttribute {
158                entity: APPROVAL,
159                id: record.id,
160                attribute: "TimeOfApproval",
161            });
162        }
163        for attribute in ["RequestingApproval", "GivingApproval"] {
164            if !record.declares(attribute) {
165                continue;
166            }
167            if let Some(target) = record.optional_ref(attribute)? {
168                record.validate_target(view.model(), attribute, target, "IfcActorSelect")?;
169            }
170        }
171        Ok(self)
172    }
173}
174
175impl<'m> ApprovalRelationship<'m> {
176    /// Optional relationship name (required in IFC2X3).
177    pub fn name(self) -> ApprovalResult<Option<&'m str>> {
178        self.record.optional_text("Name")
179    }
180    /// Optional relationship description.
181    pub fn description(self) -> ApprovalResult<Option<&'m str>> {
182        self.record.optional_text("Description")
183    }
184    /// Relating approval.
185    pub fn relating_approval(self) -> ApprovalResult<EntityId> {
186        self.record.required_ref("RelatingApproval")
187    }
188    /// Non-empty unique related approvals; IFC2X3's single
189    /// `RelatedApproval` is a one-element set.
190    pub fn related_approvals(self) -> ApprovalResult<Vec<EntityId>> {
191        self.record.required_refs("RelatedApprovals")
192    }
193    fn validate(self, view: ApprovalView<'m>) -> ApprovalResult<Self> {
194        let record = self.record;
195        required_if_declared(record, "Name")?;
196        let relating = self.relating_approval()?;
197        record.validate_target(view.model(), "RelatingApproval", relating, APPROVAL)?;
198        for target in self.related_approvals()? {
199            if target == relating {
200                return Err(ApprovalError::Semantic {
201                    entity: APPROVAL_REL,
202                    id: record.id,
203                    rule: "NoSelfRelationship",
204                    detail: format!("approval {target} appears on both ends"),
205                });
206            }
207            record.validate_target(view.model(), "RelatedApprovals", target, APPROVAL)?;
208        }
209        Ok(self)
210    }
211}
212
213impl<'m> ResourceApprovalRelationship<'m> {
214    /// Optional relationship name.
215    pub fn name(self) -> ApprovalResult<Option<&'m str>> {
216        self.record.optional_text("Name")
217    }
218    /// Optional relationship description.
219    pub fn description(self) -> ApprovalResult<Option<&'m str>> {
220        self.record.optional_text("Description")
221    }
222    /// Non-empty unique resource-select targets.
223    pub fn related_resources(self) -> ApprovalResult<Vec<EntityId>> {
224        self.record.required_refs("RelatedResourceObjects")
225    }
226    /// Approval governing the resources.
227    pub fn relating_approval(self) -> ApprovalResult<EntityId> {
228        self.record.required_ref("RelatingApproval")
229    }
230    fn validate(self, view: ApprovalView<'m>) -> ApprovalResult<Self> {
231        let record = self.record;
232        record.validate_target(
233            view.model(),
234            "RelatingApproval",
235            self.relating_approval()?,
236            APPROVAL,
237        )?;
238        for target in self.related_resources()? {
239            record.validate_target(
240                view.model(),
241                "RelatedResourceObjects",
242                target,
243                "IfcResourceObjectSelect",
244            )?;
245        }
246        Ok(self)
247    }
248}
249
250impl<'m> ApprovalAssignment<'m> {
251    /// Root GlobalId.
252    pub fn global_id(self) -> ApprovalResult<&'m str> {
253        self.record.required_text("GlobalId")
254    }
255    /// Optional relationship name.
256    pub fn name(self) -> ApprovalResult<Option<&'m str>> {
257        self.record.optional_text("Name")
258    }
259    /// Optional relationship description.
260    pub fn description(self) -> ApprovalResult<Option<&'m str>> {
261        self.record.optional_text("Description")
262    }
263    /// Related definition-select targets (`IfcRoot` under WR21 in IFC2X3).
264    pub fn related_objects(self) -> ApprovalResult<Vec<EntityId>> {
265        self.record.required_refs("RelatedObjects")
266    }
267    /// Relating approval.
268    pub fn relating_approval(self) -> ApprovalResult<EntityId> {
269        self.record.required_ref("RelatingApproval")
270    }
271    fn validate(self, view: ApprovalView<'m>) -> ApprovalResult<Self> {
272        let record = self.record;
273        if Guid::parse(self.global_id()?).is_none() {
274            return Err(ApprovalError::InvalidValue {
275                entity: ASSIGNMENT,
276                id: record.id,
277                attribute: "GlobalId",
278                value: self.global_id()?.into(),
279            });
280        }
281        record.validate_target(
282            view.model(),
283            "RelatingApproval",
284            self.relating_approval()?,
285            APPROVAL,
286        )?;
287        for target in self.related_objects()? {
288            record.validate_target(
289                view.model(),
290                "RelatedObjects",
291                target,
292                "IfcDefinitionSelect",
293            )?;
294        }
295        Ok(self)
296    }
297}
298
299impl<'m> ApprovalView<'m> {
300    fn entity(self, id: EntityId) -> ApprovalResult<&'m Entity> {
301        self.model()
302            .get(id)
303            .ok_or(ApprovalError::UnknownEntity { id })
304    }
305
306    /// Strictly project one approval.
307    pub fn approval(self, id: EntityId) -> ApprovalResult<Approval<'m>> {
308        Approval::bound(id, self.entity(id)?, self.layout()?)?.validate(self)
309    }
310    /// Strictly project one approval-to-approval relationship.
311    pub fn approval_relationship(self, id: EntityId) -> ApprovalResult<ApprovalRelationship<'m>> {
312        ApprovalRelationship::bound(id, self.entity(id)?, self.layout()?)?.validate(self)
313    }
314    /// Strictly project one resource approval relationship.
315    pub fn resource_approval_relationship(
316        self,
317        id: EntityId,
318    ) -> ApprovalResult<ResourceApprovalRelationship<'m>> {
319        ResourceApprovalRelationship::bound(id, self.entity(id)?, self.layout()?)?.validate(self)
320    }
321    /// Strictly project one rooted object approval association.
322    pub fn approval_assignment(self, id: EntityId) -> ApprovalResult<ApprovalAssignment<'m>> {
323        ApprovalAssignment::bound(id, self.entity(id)?, self.layout()?)?.validate(self)
324    }
325
326    /// Resource-select IDs directly governed by an approval.
327    ///
328    /// Empty in IFC2X3, which declares no `IfcResourceApprovalRelationship`.
329    pub fn resources_approved_by(self, approval: EntityId) -> ApprovalResult<Vec<EntityId>> {
330        self.approval(approval)?;
331        let layout = self.layout()?;
332        let mut out = Vec::new();
333        if layout.require_entity(RESOURCE_REL).is_err() {
334            return Ok(out);
335        }
336        for (id, entity) in self.model().of_type(RESOURCE_REL) {
337            let relationship = ResourceApprovalRelationship::bound(id, entity, layout)?;
338            if relationship.relating_approval()? == approval {
339                out.extend(relationship.validate(self)?.related_resources()?);
340            }
341        }
342        Ok(out)
343    }
344
345    /// Definition-select IDs directly associated with an approval.
346    pub fn objects_approved_by(self, approval: EntityId) -> ApprovalResult<Vec<EntityId>> {
347        self.approval(approval)?;
348        let layout = self.layout()?;
349        let mut out = Vec::new();
350        for (id, entity) in self.model().of_type(ASSIGNMENT) {
351            let assignment = ApprovalAssignment::bound(id, entity, layout)?;
352            if assignment.relating_approval()? == approval {
353                out.extend(assignment.validate(self)?.related_objects()?);
354            }
355        }
356        Ok(out)
357    }
358}