1use 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 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 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 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 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
86fn 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 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 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 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#[derive(Debug, Clone, Copy)]
162pub struct ExternalReferenceRelationshipDraft<'a> {
163 pub name: Option<&'a str>,
165 pub description: Option<&'a str>,
167 pub relating_reference: EntityId,
169 pub related_resources: &'a [EntityId],
171}
172
173pub 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}