1use ifc_model::{EntityId, Value};
12
13use crate::author::editor::{build_entity, text, validate_enum, ResourceEditor};
14use crate::error::{ResourceError, ResourceResult};
15
16#[derive(Debug, Clone, Copy)]
18pub struct ActorRoleDraft<'a> {
19 pub role: &'a str,
21 pub user_defined_role: Option<&'a str>,
23 pub description: Option<&'a str>,
25}
26
27#[derive(Debug, Clone, Copy, Default)]
29pub struct PostalAddressDraft<'a> {
30 pub purpose: Option<&'a str>,
32 pub description: Option<&'a str>,
34 pub user_defined_purpose: Option<&'a str>,
36 pub internal_location: Option<&'a str>,
38 pub postal_box: Option<&'a str>,
40 pub town: Option<&'a str>,
42 pub region: Option<&'a str>,
44 pub postal_code: Option<&'a str>,
46 pub country: Option<&'a str>,
48}
49
50#[derive(Debug, Clone, Copy, Default)]
52pub struct TelecomAddressDraft<'a> {
53 pub purpose: Option<&'a str>,
55 pub description: Option<&'a str>,
57 pub user_defined_purpose: Option<&'a str>,
59 pub pager_number: Option<&'a str>,
61 pub www_home_page_url: Option<&'a str>,
63}
64
65#[derive(Debug, Clone, Copy, Default)]
70pub struct TelecomLists<'a> {
71 pub telephone: &'a [&'a str],
73 pub facsimile: &'a [&'a str],
75 pub email: &'a [&'a str],
77 pub messaging: &'a [&'a str],
79}
80
81fn optional_list(values: &[&str]) -> Option<Value> {
83 if values.is_empty() {
84 return None;
85 }
86 Some(Value::List(
87 values.iter().map(|v| Value::Text((*v).into())).collect(),
88 ))
89}
90
91impl ResourceEditor<'_> {
92 pub fn create_actor_role(&mut self, draft: ActorRoleDraft<'_>) -> ResourceResult<EntityId> {
99 const ENTITY: &str = "IfcActorRole";
100 validate_enum(self.schema, ENTITY, "Role", draft.role)?;
101 if draft.role.eq_ignore_ascii_case("USERDEFINED")
102 && draft
103 .user_defined_role
104 .is_none_or(|text| text.trim().is_empty())
105 {
106 return Err(ResourceError::SemanticViolation {
107 entity: None,
108 rule: "WR1",
109 });
110 }
111 let entity = build_entity(
112 self.schema,
113 ENTITY,
114 &[
115 ("Role", Some(Value::Enum(draft.role.into()))),
116 ("UserDefinedRole", draft.user_defined_role.map(text)),
117 ("Description", draft.description.map(text)),
118 ],
119 )?;
120 self.commit_create(entity)
121 }
122
123 pub fn create_postal_address(
133 &mut self,
134 draft: PostalAddressDraft<'_>,
135 address_lines: &[&str],
136 ) -> ResourceResult<EntityId> {
137 const ENTITY: &str = "IfcPostalAddress";
138 self.validate_address_purpose(ENTITY, draft.purpose, draft.user_defined_purpose)?;
139 let locates = !address_lines.is_empty()
141 || draft.internal_location.is_some()
142 || draft.postal_box.is_some()
143 || draft.postal_code.is_some()
144 || draft.town.is_some()
145 || draft.region.is_some()
146 || draft.country.is_some();
147 if !locates {
148 return Err(ResourceError::SemanticViolation {
149 entity: None,
150 rule: "WR1",
151 });
152 }
153 let entity = build_entity(
154 self.schema,
155 ENTITY,
156 &[
157 ("Purpose", draft.purpose.map(|p| Value::Enum(p.into()))),
158 ("Description", draft.description.map(text)),
159 ("UserDefinedPurpose", draft.user_defined_purpose.map(text)),
160 ("InternalLocation", draft.internal_location.map(text)),
161 ("AddressLines", optional_list(address_lines)),
162 ("PostalBox", draft.postal_box.map(text)),
163 ("Town", draft.town.map(text)),
164 ("Region", draft.region.map(text)),
165 ("PostalCode", draft.postal_code.map(text)),
166 ("Country", draft.country.map(text)),
167 ],
168 )?;
169 self.commit_create(entity)
170 }
171
172 pub fn create_telecom_address(
180 &mut self,
181 draft: TelecomAddressDraft<'_>,
182 lists: TelecomLists<'_>,
183 ) -> ResourceResult<EntityId> {
184 const ENTITY: &str = "IfcTelecomAddress";
185 self.validate_address_purpose(ENTITY, draft.purpose, draft.user_defined_purpose)?;
186 let reachable = !lists.telephone.is_empty()
188 || !lists.facsimile.is_empty()
189 || !lists.email.is_empty()
190 || !lists.messaging.is_empty()
191 || draft.pager_number.is_some()
192 || draft.www_home_page_url.is_some();
193 if !reachable {
194 return Err(ResourceError::SemanticViolation {
195 entity: None,
196 rule: "MinimumDataProvided",
197 });
198 }
199 let entity = build_entity(
200 self.schema,
201 ENTITY,
202 &[
203 ("Purpose", draft.purpose.map(|p| Value::Enum(p.into()))),
204 ("Description", draft.description.map(text)),
205 ("UserDefinedPurpose", draft.user_defined_purpose.map(text)),
206 ("TelephoneNumbers", optional_list(lists.telephone)),
207 ("FacsimileNumbers", optional_list(lists.facsimile)),
208 ("PagerNumber", draft.pager_number.map(text)),
209 ("ElectronicMailAddresses", optional_list(lists.email)),
210 ("WWWHomePageURL", draft.www_home_page_url.map(text)),
211 ("MessagingIDs", optional_list(lists.messaging)),
212 ],
213 )?;
214 self.commit_create(entity)
215 }
216
217 fn validate_address_purpose(
219 &self,
220 entity: &'static str,
221 purpose: Option<&str>,
222 user_defined: Option<&str>,
223 ) -> ResourceResult<()> {
224 let Some(token) = purpose else {
225 return Ok(());
226 };
227 validate_enum(self.schema, entity, "Purpose", token)?;
228 if token.eq_ignore_ascii_case("USERDEFINED")
229 && user_defined.is_none_or(|text| text.trim().is_empty())
230 {
231 return Err(ResourceError::SemanticViolation {
232 entity: None,
233 rule: "USERDEFINED_REQUIRES_USER_DEFINED_PURPOSE",
234 });
235 }
236 Ok(())
237 }
238
239 pub fn create_organization_relationship(
246 &mut self,
247 name: Option<&str>,
248 description: Option<&str>,
249 relating: EntityId,
250 related: &[EntityId],
251 ) -> ResourceResult<EntityId> {
252 const ENTITY: &str = "IfcOrganizationRelationship";
253 self.check_reference(
254 relating,
255 "RelatingOrganization",
256 "IfcOrganization",
257 relating,
258 )?;
259 if related.is_empty() {
260 return Err(ResourceError::InvalidDraft {
261 entity_type: ENTITY,
262 attribute: "RelatedOrganizations",
263 expected: "at least one organisation, per SET [1:?]",
264 });
265 }
266 for organization in related {
267 self.check_reference(
268 *organization,
269 "RelatedOrganizations",
270 "IfcOrganization",
271 *organization,
272 )?;
273 if *organization == relating {
276 return Err(ResourceError::SemanticViolation {
277 entity: None,
278 rule: "RELATING_ORGANIZATION_IS_NOT_RELATED",
279 });
280 }
281 }
282 let entity = build_entity(
283 self.schema,
284 ENTITY,
285 &[
286 ("Name", name.map(text)),
287 ("Description", description.map(text)),
288 ("RelatingOrganization", Some(Value::Ref(relating))),
289 (
290 "RelatedOrganizations",
291 Some(Value::List(
292 related.iter().copied().map(Value::Ref).collect(),
293 )),
294 ),
295 ],
296 )?;
297 self.commit_create(entity)
298 }
299}