Skip to main content

ifc_classification/
external_relationship.rs

1//! Generic `IfcExternalReferenceRelationship` projection and authoring.
2
3use std::collections::HashSet;
4
5use ifc_model::{Entity, EntityId, Model, Transaction, Value};
6
7use crate::authoring::{final_type, text};
8use crate::view::{
9    borrowed_entity, optional_text, required_ref, required_refs, ClassificationView,
10};
11use crate::{ClassificationError, ClassificationResult};
12
13const KIND: &str = "IFCEXTERNALREFERENCERELATIONSHIP";
14
15borrowed_entity!(
16    ExternalReferenceRelationship,
17    "IFCEXTERNALREFERENCERELATIONSHIP"
18);
19
20impl<'m> ExternalReferenceRelationship<'m> {
21    /// Optional relationship name.
22    pub fn name(self) -> ClassificationResult<Option<&'m str>> {
23        optional_text(
24            KIND,
25            self.id(),
26            self.entity(),
27            self.text_slot("Name")?,
28            "Name",
29        )
30    }
31
32    /// Optional relationship description.
33    pub fn description(self) -> ClassificationResult<Option<&'m str>> {
34        optional_text(
35            KIND,
36            self.id(),
37            self.entity(),
38            self.text_slot("Description")?,
39            "Description",
40        )
41    }
42
43    /// External reference that applies to the related resources.
44    pub fn relating_reference(self) -> ClassificationResult<EntityId> {
45        required_ref(
46            KIND,
47            self.id(),
48            self.entity(),
49            self.slot("RelatingReference")?,
50            "RelatingReference",
51        )
52    }
53
54    /// Resource-level objects carrying this external reference.
55    pub fn related_resources(self) -> ClassificationResult<Vec<EntityId>> {
56        required_refs(
57            KIND,
58            self.id(),
59            self.entity(),
60            self.slot("RelatedResourceObjects")?,
61            "RelatedResourceObjects",
62        )
63    }
64
65    fn validate(self, model: &Model) -> ClassificationResult<Self> {
66        validate_target(
67            self,
68            model,
69            "RelatingReference",
70            self.relating_reference()?,
71            "IfcExternalReference",
72        )?;
73        for target in self.related_resources()? {
74            validate_target(
75                self,
76                model,
77                "RelatedResourceObjects",
78                target,
79                "IfcResourceObjectSelect",
80            )?;
81        }
82        Ok(self)
83    }
84}
85
86/// Check `target` against `expected` in the release this relationship is
87/// read against. IFC2X3 has no `IfcExternalReferenceRelationship`, so any
88/// read there already fails `NotInSchema` before this is reached.
89fn validate_target(
90    relationship: ExternalReferenceRelationship<'_>,
91    model: &Model,
92    attribute: &'static str,
93    target: EntityId,
94    expected: &'static str,
95) -> ClassificationResult<()> {
96    let relation = relationship.id();
97    let entity = model
98        .get(target)
99        .ok_or(ClassificationError::DanglingReference {
100            entity: KIND,
101            id: relation,
102            attribute,
103            target,
104        })?;
105    let (_, schema) = relationship.release().bound()?;
106    if schema.accepts_type(expected, &entity.type_name) {
107        Ok(())
108    } else {
109        Err(ClassificationError::ReferenceType {
110            entity: KIND,
111            id: relation,
112            attribute,
113            target,
114            expected,
115            actual: entity.type_name.to_string(),
116        })
117    }
118}
119
120impl<'m> ClassificationView<'m> {
121    /// Strictly project one generic external-reference relationship.
122    pub fn external_reference_relationship(
123        self,
124        id: EntityId,
125    ) -> ClassificationResult<ExternalReferenceRelationship<'m>> {
126        let entity = self
127            .model()
128            .get(id)
129            .ok_or(ClassificationError::UnknownEntity { id })?;
130        ExternalReferenceRelationship::try_bound(id, entity, self.release())?.validate(self.model())
131    }
132
133    /// All generic external-reference relationships in deterministic model order.
134    pub fn external_reference_relationships(
135        self,
136    ) -> impl Iterator<Item = ExternalReferenceRelationship<'m>> + 'm {
137        self.model().of_type(KIND).map(move |(id, entity)| {
138            ExternalReferenceRelationship::from_known(id, entity, self.release())
139        })
140    }
141
142    /// Valid external-reference relationships naming a particular resource.
143    pub fn external_references_for(
144        self,
145        resource: EntityId,
146    ) -> ClassificationResult<Vec<ExternalReferenceRelationship<'m>>> {
147        if self.model().get(resource).is_none() {
148            return Err(ClassificationError::UnknownEntity { id: resource });
149        }
150        let mut out = Vec::new();
151        for relationship in self.external_reference_relationships() {
152            if relationship.related_resources()?.contains(&resource) {
153                out.push(relationship.validate(self.model())?);
154            }
155        }
156        Ok(out)
157    }
158}
159
160/// Draft for one IFC4 generic external-reference relationship.
161#[derive(Debug, Clone, Copy)]
162pub struct ExternalReferenceRelationshipDraft<'a> {
163    /// Optional relationship name.
164    pub name: Option<&'a str>,
165    /// Optional relationship description.
166    pub description: Option<&'a str>,
167    /// Existing or earlier-staged subtype of `IfcExternalReference`.
168    pub relating_reference: EntityId,
169    /// Non-empty unique `IfcResourceObjectSelect` targets.
170    pub related_resources: &'a [EntityId],
171}
172
173/// Validate and stage one `IfcExternalReferenceRelationship`.
174pub fn create_external_reference_relationship(
175    tx: &mut Transaction,
176    model: &Model,
177    draft: ExternalReferenceRelationshipDraft<'_>,
178) -> ClassificationResult<EntityId> {
179    if draft.related_resources.is_empty() {
180        return Err(ClassificationError::AuthoringInvalid {
181            entity: KIND,
182            attribute: "RelatedResourceObjects",
183            value: "empty SET [1:?]".into(),
184        });
185    }
186    validate_draft_target(tx, model, draft.relating_reference, "IfcExternalReference")?;
187    let mut seen = HashSet::new();
188    for &target in draft.related_resources {
189        if !seen.insert(target) {
190            return Err(ClassificationError::AuthoringInvalid {
191                entity: KIND,
192                attribute: "RelatedResourceObjects",
193                value: format!("duplicate {target}"),
194            });
195        }
196        validate_draft_target(tx, model, target, "IfcResourceObjectSelect")?;
197    }
198    Ok(tx.create(Entity::new(
199        KIND,
200        vec![
201            draft.name.map_or(Value::Null, text),
202            draft.description.map_or(Value::Null, text),
203            Value::Ref(draft.relating_reference),
204            Value::List(
205                draft
206                    .related_resources
207                    .iter()
208                    .copied()
209                    .map(Value::Ref)
210                    .collect(),
211            ),
212        ],
213    )))
214}
215
216fn validate_draft_target(
217    tx: &Transaction,
218    model: &Model,
219    target: EntityId,
220    expected: &'static str,
221) -> ClassificationResult<()> {
222    let actual =
223        final_type(tx, model, target).ok_or(ClassificationError::UnknownEntity { id: target })?;
224    if ifc_schema::ifc4().accepts_type(expected, actual) {
225        Ok(())
226    } else {
227        Err(ClassificationError::AuthoringReferenceType {
228            target,
229            expected,
230            actual: actual.into(),
231        })
232    }
233}