1use 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 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 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 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 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
75fn 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 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 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 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#[derive(Debug, Clone, Copy)]
152pub struct ExternalReferenceRelationshipDraft<'a> {
153 pub name: Option<&'a str>,
155 pub description: Option<&'a str>,
157 pub relating_reference: EntityId,
159 pub related_resources: &'a [EntityId],
162}
163
164pub 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}