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)]
152pub struct ExternalReferenceRelationshipDraft<'a> {
153    /// Optional relationship name.
154    pub name: Option<&'a str>,
155    /// Optional relationship description.
156    pub description: Option<&'a str>,
157    /// Existing or earlier-staged subtype of `IfcExternalReference`.
158    pub relating_reference: EntityId,
159    /// Non-empty unique `IfcResourceObjectSelect` targets, as the model's
160    /// release declares that select (IFC4X3 adds `IfcShapeAspect`).
161    pub related_resources: &'a [EntityId],
162}
163
164/// Validate and stage one `IfcExternalReferenceRelationship` in the layout
165/// of `model`'s declared release.
166///
167/// # Errors
168///
169/// An empty or duplicated related set; a reference the release's declared
170/// type does not accept; an IFC2X3 model, which has no such entity
171/// (`EntityNotInSchema`); and a header binding no single known release.
172/// Nothing is staged on an error.
173pub fn create_external_reference_relationship(
174    tx: &mut Transaction,
175    model: &Model,
176    draft: ExternalReferenceRelationshipDraft<'_>,
177) -> ClassificationResult<EntityId> {
178    let release = Release::of(model);
179    release.require_entity(KIND)?;
180    if draft.related_resources.is_empty() {
181        return Err(ClassificationError::AuthoringInvalid {
182            entity: KIND,
183            attribute: "RelatedResourceObjects",
184            value: "empty SET [1:?]".into(),
185        });
186    }
187    require_accepts(
188        tx,
189        model,
190        release,
191        KIND,
192        "RelatingReference",
193        draft.relating_reference,
194    )?;
195    let mut seen = HashSet::new();
196    for &target in draft.related_resources {
197        if !seen.insert(target) {
198            return Err(ClassificationError::AuthoringInvalid {
199                entity: KIND,
200                attribute: "RelatedResourceObjects",
201                value: format!("duplicate {target}"),
202            });
203        }
204        require_accepts(tx, model, release, KIND, "RelatedResourceObjects", target)?;
205    }
206    let record = release.record(
207        KIND,
208        vec![
209            ("Name", draft.name.map_or(Value::Null, text)),
210            ("Description", draft.description.map_or(Value::Null, text)),
211            ("RelatingReference", Value::Ref(draft.relating_reference)),
212            (
213                "RelatedResourceObjects",
214                Value::List(
215                    draft
216                        .related_resources
217                        .iter()
218                        .copied()
219                        .map(Value::Ref)
220                        .collect(),
221                ),
222            ),
223        ],
224    )?;
225    Ok(tx.create(record))
226}