1use std::collections::HashSet;
4use std::sync::Arc;
5
6use ifc_model::guid::Guid;
7use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
8
9use crate::{ClassificationError, ClassificationResult};
10
11#[derive(Debug, Clone, Copy)]
13pub struct ClassificationDraft<'a> {
14 pub source: Option<&'a str>,
16 pub edition: Option<&'a str>,
18 pub edition_date: Option<&'a str>,
20 pub name: &'a str,
22 pub description: Option<&'a str>,
24 pub location: Option<&'a str>,
26 pub reference_tokens: Option<&'a [&'a str]>,
28}
29
30#[derive(Debug, Clone, Copy)]
32pub struct ClassificationReferenceDraft<'a> {
33 pub location: Option<&'a str>,
35 pub identification: Option<&'a str>,
37 pub name: Option<&'a str>,
39 pub referenced_source: Option<EntityId>,
41 pub description: Option<&'a str>,
43 pub sort: Option<&'a str>,
45}
46
47#[derive(Debug, Clone, Copy)]
49pub struct DocumentDraft<'a> {
50 pub identification: &'a str,
52 pub name: &'a str,
54 pub description: Option<&'a str>,
56 pub location: Option<&'a str>,
58 pub purpose: Option<&'a str>,
60 pub intended_use: Option<&'a str>,
62 pub scope: Option<&'a str>,
64 pub revision: Option<&'a str>,
66 pub document_owner: Option<EntityId>,
68 pub editors: Option<&'a [EntityId]>,
70 pub creation_time: Option<&'a str>,
72 pub last_revision_time: Option<&'a str>,
74 pub electronic_format: Option<&'a str>,
76 pub valid_from: Option<&'a str>,
78 pub valid_until: Option<&'a str>,
80 pub confidentiality: Option<&'a str>,
82 pub status: Option<&'a str>,
84}
85
86#[derive(Debug, Clone, Copy)]
88pub struct DocumentReferenceDraft<'a> {
89 pub location: Option<&'a str>,
91 pub identification: Option<&'a str>,
93 pub name: Option<&'a str>,
95 pub description: Option<&'a str>,
97 pub referenced_document: Option<EntityId>,
99}
100
101#[derive(Debug, Clone, Copy)]
103pub struct LibraryDraft<'a> {
104 pub name: &'a str,
106 pub version: Option<&'a str>,
108 pub publisher: Option<EntityId>,
110 pub version_date: Option<&'a str>,
112 pub location: Option<&'a str>,
114 pub description: Option<&'a str>,
116}
117
118#[derive(Debug, Clone, Copy)]
120pub struct LibraryReferenceDraft<'a> {
121 pub location: Option<&'a str>,
123 pub identification: Option<&'a str>,
125 pub name: Option<&'a str>,
127 pub description: Option<&'a str>,
129 pub language: Option<&'a str>,
131 pub referenced_library: Option<EntityId>,
133}
134
135#[derive(Debug, Clone, Copy)]
137pub struct AssociationDraft<'a> {
138 pub global_id: &'a str,
140 pub name: Option<&'a str>,
142 pub description: Option<&'a str>,
144 pub related_objects: &'a [EntityId],
146}
147
148pub(crate) fn text(value: &str) -> Value {
149 Value::Text(Arc::from(value))
150}
151fn optional_text(value: Option<&str>) -> Value {
152 value.map_or(Value::Null, text)
153}
154fn optional_ref(value: Option<EntityId>) -> Value {
155 value.map_or(Value::Null, Value::Ref)
156}
157fn refs(values: &[EntityId]) -> Value {
158 Value::List(values.iter().copied().map(Value::Ref).collect())
159}
160fn optional_enum(value: Option<&str>) -> Value {
161 value.map_or(Value::Null, |v| Value::Enum(Arc::from(v)))
162}
163
164pub(crate) fn final_type<'a>(
165 tx: &'a Transaction,
166 model: &'a Model,
167 id: EntityId,
168) -> Option<&'a str> {
169 for edit in tx.edits().iter().rev() {
170 match edit {
171 Edit::Create {
172 id: edit_id,
173 entity,
174 } if *edit_id == id => return Some(&entity.type_name),
175 Edit::Retype {
176 id: edit_id,
177 type_name,
178 } if *edit_id == id => return Some(type_name),
179 Edit::Remove { id: edit_id } if *edit_id == id => return None,
180 _ => {}
181 }
182 }
183 model.get(id).map(|entity| entity.type_name.as_ref())
184}
185
186fn require_type(
187 tx: &Transaction,
188 model: &Model,
189 id: EntityId,
190 expected: &'static [&'static str],
191 label: &'static str,
192) -> ClassificationResult<()> {
193 let actual = final_type(tx, model, id).ok_or(ClassificationError::UnknownEntity { id })?;
194 if expected
195 .iter()
196 .any(|kind| actual.eq_ignore_ascii_case(kind))
197 {
198 return Ok(());
199 }
200 Err(ClassificationError::AuthoringReferenceType {
201 target: id,
202 expected: label,
203 actual: actual.to_owned(),
204 })
205}
206
207fn require_actor(tx: &Transaction, model: &Model, id: EntityId) -> ClassificationResult<()> {
208 require_type(
209 tx,
210 model,
211 id,
212 &["IFCORGANIZATION", "IFCPERSON", "IFCPERSONANDORGANIZATION"],
213 "IfcActorSelect",
214 )
215}
216
217fn require_definition(tx: &Transaction, model: &Model, id: EntityId) -> ClassificationResult<()> {
218 let actual = final_type(tx, model, id).ok_or(ClassificationError::UnknownEntity { id })?;
219 let schema = ifc_schema::ifc4();
220 if schema.is_a(actual, "IFCOBJECTDEFINITION") || schema.is_a(actual, "IFCPROPERTYDEFINITION") {
221 Ok(())
222 } else {
223 Err(ClassificationError::AuthoringReferenceType {
224 target: id,
225 expected: "IfcDefinitionSelect",
226 actual: actual.to_owned(),
227 })
228 }
229}
230
231fn require_external_identity(
232 entity: &'static str,
233 location: Option<&str>,
234 identification: Option<&str>,
235 name: Option<&str>,
236) -> ClassificationResult<()> {
237 if location.is_some() || identification.is_some() || name.is_some() {
238 Ok(())
239 } else {
240 Err(ClassificationError::AuthoringInvalid {
241 entity,
242 attribute: "WR1",
243 value: "Location, Identification, and Name are all unstated".into(),
244 })
245 }
246}
247
248fn require_enum(
249 entity: &'static str,
250 attribute: &'static str,
251 value: Option<&str>,
252 allowed: &[&str],
253) -> ClassificationResult<()> {
254 if let Some(value) = value {
255 if !allowed
256 .iter()
257 .any(|candidate| candidate.eq_ignore_ascii_case(value))
258 {
259 return Err(ClassificationError::AuthoringInvalid {
260 entity,
261 attribute,
262 value: value.into(),
263 });
264 }
265 }
266 Ok(())
267}
268
269pub fn create_classification(
271 tx: &mut Transaction,
272 draft: ClassificationDraft<'_>,
273) -> ClassificationResult<EntityId> {
274 if draft
275 .reference_tokens
276 .is_some_and(|tokens| tokens.is_empty())
277 {
278 return Err(ClassificationError::AuthoringInvalid {
279 entity: "IFCCLASSIFICATION",
280 attribute: "ReferenceTokens",
281 value: "empty LIST [1:?]".into(),
282 });
283 }
284 let tokens = draft.reference_tokens.map_or(Value::Null, |items| {
285 Value::List(items.iter().map(|v| text(v)).collect())
286 });
287 Ok(tx.create(Entity::new(
288 "IFCCLASSIFICATION",
289 vec![
290 optional_text(draft.source),
291 optional_text(draft.edition),
292 optional_text(draft.edition_date),
293 text(draft.name),
294 optional_text(draft.description),
295 optional_text(draft.location),
296 tokens,
297 ],
298 )))
299}
300
301pub fn create_classification_reference(
303 tx: &mut Transaction,
304 model: &Model,
305 draft: ClassificationReferenceDraft<'_>,
306) -> ClassificationResult<EntityId> {
307 require_external_identity(
308 "IFCCLASSIFICATIONREFERENCE",
309 draft.location,
310 draft.identification,
311 draft.name,
312 )?;
313 if let Some(source) = draft.referenced_source {
314 require_type(
315 tx,
316 model,
317 source,
318 &["IFCCLASSIFICATION", "IFCCLASSIFICATIONREFERENCE"],
319 "IfcClassificationReferenceSelect",
320 )?;
321 }
322 Ok(tx.create(Entity::new(
323 "IFCCLASSIFICATIONREFERENCE",
324 vec![
325 optional_text(draft.location),
326 optional_text(draft.identification),
327 optional_text(draft.name),
328 optional_ref(draft.referenced_source),
329 optional_text(draft.description),
330 optional_text(draft.sort),
331 ],
332 )))
333}
334
335pub fn create_document(
337 tx: &mut Transaction,
338 model: &Model,
339 draft: DocumentDraft<'_>,
340) -> ClassificationResult<EntityId> {
341 require_enum(
342 "IFCDOCUMENTINFORMATION",
343 "Confidentiality",
344 draft.confidentiality,
345 &[
346 "PUBLIC",
347 "RESTRICTED",
348 "CONFIDENTIAL",
349 "PERSONAL",
350 "USERDEFINED",
351 "NOTDEFINED",
352 ],
353 )?;
354 require_enum(
355 "IFCDOCUMENTINFORMATION",
356 "Status",
357 draft.status,
358 &["DRAFT", "FINAL", "REVISION", "NOTDEFINED"],
359 )?;
360 if let Some(owner) = draft.document_owner {
361 require_actor(tx, model, owner)?;
362 }
363 if let Some(editors) = draft.editors {
364 if editors.is_empty() {
365 return Err(ClassificationError::AuthoringInvalid {
366 entity: "IFCDOCUMENTINFORMATION",
367 attribute: "Editors",
368 value: "empty SET [1:?]".into(),
369 });
370 }
371 let mut seen = HashSet::new();
372 for &editor in editors {
373 if !seen.insert(editor) {
374 return Err(ClassificationError::AuthoringInvalid {
375 entity: "IFCDOCUMENTINFORMATION",
376 attribute: "Editors",
377 value: format!("duplicate {editor}"),
378 });
379 }
380 require_actor(tx, model, editor)?;
381 }
382 }
383 let editors = draft.editors.map_or(Value::Null, refs);
384 Ok(tx.create(Entity::new(
385 "IFCDOCUMENTINFORMATION",
386 vec![
387 text(draft.identification),
388 text(draft.name),
389 optional_text(draft.description),
390 optional_text(draft.location),
391 optional_text(draft.purpose),
392 optional_text(draft.intended_use),
393 optional_text(draft.scope),
394 optional_text(draft.revision),
395 optional_ref(draft.document_owner),
396 editors,
397 optional_text(draft.creation_time),
398 optional_text(draft.last_revision_time),
399 optional_text(draft.electronic_format),
400 optional_text(draft.valid_from),
401 optional_text(draft.valid_until),
402 optional_enum(draft.confidentiality),
403 optional_enum(draft.status),
404 ],
405 )))
406}
407
408pub fn create_document_reference(
410 tx: &mut Transaction,
411 model: &Model,
412 draft: DocumentReferenceDraft<'_>,
413) -> ClassificationResult<EntityId> {
414 require_external_identity(
415 "IFCDOCUMENTREFERENCE",
416 draft.location,
417 draft.identification,
418 draft.name,
419 )?;
420 if !(draft.name.is_some() ^ draft.referenced_document.is_some()) {
421 return Err(ClassificationError::AuthoringInvalid {
422 entity: "IFCDOCUMENTREFERENCE",
423 attribute: "WR1",
424 value: "exactly one of Name and ReferencedDocument must be stated".into(),
425 });
426 }
427 if let Some(document) = draft.referenced_document {
428 require_type(
429 tx,
430 model,
431 document,
432 &["IFCDOCUMENTINFORMATION"],
433 "IfcDocumentInformation",
434 )?;
435 }
436 Ok(tx.create(Entity::new(
437 "IFCDOCUMENTREFERENCE",
438 vec![
439 optional_text(draft.location),
440 optional_text(draft.identification),
441 optional_text(draft.name),
442 optional_text(draft.description),
443 optional_ref(draft.referenced_document),
444 ],
445 )))
446}
447
448pub fn create_library(
450 tx: &mut Transaction,
451 model: &Model,
452 draft: LibraryDraft<'_>,
453) -> ClassificationResult<EntityId> {
454 if let Some(publisher) = draft.publisher {
455 require_actor(tx, model, publisher)?;
456 }
457 Ok(tx.create(Entity::new(
458 "IFCLIBRARYINFORMATION",
459 vec![
460 text(draft.name),
461 optional_text(draft.version),
462 optional_ref(draft.publisher),
463 optional_text(draft.version_date),
464 optional_text(draft.location),
465 optional_text(draft.description),
466 ],
467 )))
468}
469
470pub fn create_library_reference(
472 tx: &mut Transaction,
473 model: &Model,
474 draft: LibraryReferenceDraft<'_>,
475) -> ClassificationResult<EntityId> {
476 require_external_identity(
477 "IFCLIBRARYREFERENCE",
478 draft.location,
479 draft.identification,
480 draft.name,
481 )?;
482 if let Some(library) = draft.referenced_library {
483 require_type(
484 tx,
485 model,
486 library,
487 &["IFCLIBRARYINFORMATION"],
488 "IfcLibraryInformation",
489 )?;
490 }
491 Ok(tx.create(Entity::new(
492 "IFCLIBRARYREFERENCE",
493 vec![
494 optional_text(draft.location),
495 optional_text(draft.identification),
496 optional_text(draft.name),
497 optional_text(draft.description),
498 optional_text(draft.language),
499 optional_ref(draft.referenced_library),
500 ],
501 )))
502}
503
504fn associate(
505 tx: &mut Transaction,
506 model: &Model,
507 draft: AssociationDraft<'_>,
508 target: EntityId,
509 kind: &'static str,
510 target_types: &'static [&'static str],
511 label: &'static str,
512) -> ClassificationResult<EntityId> {
513 if Guid::parse(draft.global_id).is_none() {
514 return Err(ClassificationError::AuthoringInvalid {
515 entity: kind,
516 attribute: "GlobalId",
517 value: draft.global_id.into(),
518 });
519 }
520 if draft.related_objects.is_empty() {
521 return Err(ClassificationError::AuthoringInvalid {
522 entity: kind,
523 attribute: "RelatedObjects",
524 value: "empty SET [1:?]".into(),
525 });
526 }
527 let mut seen = HashSet::new();
528 for &object in draft.related_objects {
529 if !seen.insert(object) {
530 return Err(ClassificationError::AuthoringInvalid {
531 entity: kind,
532 attribute: "RelatedObjects",
533 value: format!("duplicate {object}"),
534 });
535 }
536 require_definition(tx, model, object)?;
537 }
538 require_type(tx, model, target, target_types, label)?;
539 Ok(tx.create(Entity::new(
540 kind,
541 vec![
542 text(draft.global_id),
543 Value::Null,
544 optional_text(draft.name),
545 optional_text(draft.description),
546 refs(draft.related_objects),
547 Value::Ref(target),
548 ],
549 )))
550}
551
552pub fn associate_classification(
554 tx: &mut Transaction,
555 model: &Model,
556 draft: AssociationDraft<'_>,
557 target: EntityId,
558) -> ClassificationResult<EntityId> {
559 associate(
560 tx,
561 model,
562 draft,
563 target,
564 "IFCRELASSOCIATESCLASSIFICATION",
565 &["IFCCLASSIFICATION", "IFCCLASSIFICATIONREFERENCE"],
566 "IfcClassificationSelect",
567 )
568}
569pub fn associate_document(
571 tx: &mut Transaction,
572 model: &Model,
573 draft: AssociationDraft<'_>,
574 target: EntityId,
575) -> ClassificationResult<EntityId> {
576 associate(
577 tx,
578 model,
579 draft,
580 target,
581 "IFCRELASSOCIATESDOCUMENT",
582 &["IFCDOCUMENTINFORMATION", "IFCDOCUMENTREFERENCE"],
583 "IfcDocumentSelect",
584 )
585}
586pub fn associate_library(
588 tx: &mut Transaction,
589 model: &Model,
590 draft: AssociationDraft<'_>,
591 target: EntityId,
592) -> ClassificationResult<EntityId> {
593 associate(
594 tx,
595 model,
596 draft,
597 target,
598 "IFCRELASSOCIATESLIBRARY",
599 &["IFCLIBRARYINFORMATION", "IFCLIBRARYREFERENCE"],
600 "IfcLibrarySelect",
601 )
602}
603
604pub fn relate_documents(
615 tx: &mut Transaction,
616 model: &Model,
617 relating: EntityId,
618 related: &[EntityId],
619 relationship_type: Option<&str>,
620) -> ClassificationResult<EntityId> {
621 const ENTITY: &str = "IFCDOCUMENTINFORMATIONRELATIONSHIP";
622 const DOCUMENT: &[&str] = &["IFCDOCUMENTINFORMATION"];
623 require_type(tx, model, relating, DOCUMENT, "IfcDocumentInformation")?;
624 if related.is_empty() {
625 return Err(ClassificationError::AuthoringInvalid {
626 entity: ENTITY,
627 attribute: "RelatedDocuments",
628 value: "expected at least one document, per SET [1:?]".to_owned(),
629 });
630 }
631 for document in related {
632 require_type(tx, model, *document, DOCUMENT, "IfcDocumentInformation")?;
633 if *document == relating {
635 return Err(ClassificationError::AuthoringInvalid {
636 entity: ENTITY,
637 attribute: "RelatedDocuments",
638 value: "expected a document other than the relating one".to_owned(),
639 });
640 }
641 }
642 let mut attributes = vec![Value::Null; 5];
643 attributes[2] = Value::Ref(relating);
644 attributes[3] = Value::List(related.iter().copied().map(Value::Ref).collect());
645 attributes[4] = relationship_type.map_or(Value::Null, |t| Value::Text(t.into()));
646 Ok(tx.create(Entity::new(ENTITY, attributes)))
647}