Skip to main content

ifc_classification/
external_relationship.rs

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