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