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(KIND, self.id(), self.entity(), 0, "Name")
24 }
25
26 pub fn description(self) -> ClassificationResult<Option<&'m str>> {
28 optional_text(KIND, self.id(), self.entity(), 1, "Description")
29 }
30
31 pub fn relating_reference(self) -> ClassificationResult<EntityId> {
33 required_ref(KIND, self.id(), self.entity(), 2, "RelatingReference")
34 }
35
36 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 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 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 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#[derive(Debug, Clone, Copy)]
133pub struct ExternalReferenceRelationshipDraft<'a> {
134 pub name: Option<&'a str>,
136 pub description: Option<&'a str>,
138 pub relating_reference: EntityId,
140 pub related_resources: &'a [EntityId],
142}
143
144pub 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}