Skip to main content

ifc_classification/
authoring.rs

1//! Transactional IFC4 classification/document/library authoring.
2
3use 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/// Draft for one IFC4 `IfcClassification`.
12#[derive(Debug, Clone, Copy)]
13pub struct ClassificationDraft<'a> {
14    /// `Source` publishing organization, when stated.
15    pub source: Option<&'a str>,
16    /// `Edition` identifier of the classification, when stated.
17    pub edition: Option<&'a str>,
18    /// `EditionDate` (IFC date string), when stated.
19    pub edition_date: Option<&'a str>,
20    /// Required `Name` of the classification system.
21    pub name: &'a str,
22    /// `Description` of the classification, when stated.
23    pub description: Option<&'a str>,
24    /// `Location` (URI) of the classification, when stated.
25    pub location: Option<&'a str>,
26    /// `ReferenceTokens` delimiter set; must be non-empty when given.
27    pub reference_tokens: Option<&'a [&'a str]>,
28}
29
30/// Draft for one IFC4 `IfcClassificationReference`.
31#[derive(Debug, Clone, Copy)]
32pub struct ClassificationReferenceDraft<'a> {
33    /// `Location` (URI) of the reference; at least one of location/identification/name must be given.
34    pub location: Option<&'a str>,
35    /// `Identification` code within the classification, when stated.
36    pub identification: Option<&'a str>,
37    /// `Name` of the referenced item, when stated.
38    pub name: Option<&'a str>,
39    /// `ReferencedSource`: the parent `IfcClassification` or `IfcClassificationReference`, when stated.
40    pub referenced_source: Option<EntityId>,
41    /// `Description` of the reference, when stated.
42    pub description: Option<&'a str>,
43    /// `Sort` order token, when stated.
44    pub sort: Option<&'a str>,
45}
46
47/// Draft for one IFC4 `IfcDocumentInformation`.
48#[derive(Debug, Clone, Copy)]
49pub struct DocumentDraft<'a> {
50    /// Required `Identification` code of the document.
51    pub identification: &'a str,
52    /// Required `Name` of the document.
53    pub name: &'a str,
54    /// `Description` of the document, when stated.
55    pub description: Option<&'a str>,
56    /// `Location` (URI) of the document, when stated.
57    pub location: Option<&'a str>,
58    /// `Purpose` of the document, when stated.
59    pub purpose: Option<&'a str>,
60    /// `IntendedUse` of the document, when stated.
61    pub intended_use: Option<&'a str>,
62    /// `Scope` of the document, when stated.
63    pub scope: Option<&'a str>,
64    /// `Revision` identifier, when stated.
65    pub revision: Option<&'a str>,
66    /// `DocumentOwner`: an `IfcActorSelect`, when stated.
67    pub document_owner: Option<EntityId>,
68    /// `Editors`: non-empty unique set of `IfcActorSelect` ids, when stated.
69    pub editors: Option<&'a [EntityId]>,
70    /// `CreationTime` (IFC date-time string), when stated.
71    pub creation_time: Option<&'a str>,
72    /// `LastRevisionTime` (IFC date-time string), when stated.
73    pub last_revision_time: Option<&'a str>,
74    /// `ElectronicFormat`, when stated.
75    pub electronic_format: Option<&'a str>,
76    /// `ValidFrom` (IFC date string), when stated.
77    pub valid_from: Option<&'a str>,
78    /// `ValidUntil` (IFC date string), when stated.
79    pub valid_until: Option<&'a str>,
80    /// `Confidentiality` enumerator; must be one of the `IfcDocumentConfidentialityEnum` values.
81    pub confidentiality: Option<&'a str>,
82    /// `Status` enumerator; must be one of the `IfcDocumentStatusEnum` values.
83    pub status: Option<&'a str>,
84}
85
86/// Draft for one IFC4 `IfcDocumentReference`.
87#[derive(Debug, Clone, Copy)]
88pub struct DocumentReferenceDraft<'a> {
89    /// `Location` (URI) of the reference; at least one of location/identification/name must be given.
90    pub location: Option<&'a str>,
91    /// `Identification` code, when stated.
92    pub identification: Option<&'a str>,
93    /// `Name`; exactly one of `name` and `referenced_document` must be given.
94    pub name: Option<&'a str>,
95    /// `Description` of the reference, when stated.
96    pub description: Option<&'a str>,
97    /// `ReferencedDocument`: an `IfcDocumentInformation`; exactly one of `name` and this must be given.
98    pub referenced_document: Option<EntityId>,
99}
100
101/// Draft for one IFC4 `IfcLibraryInformation`.
102#[derive(Debug, Clone, Copy)]
103pub struct LibraryDraft<'a> {
104    /// Required `Name` of the library.
105    pub name: &'a str,
106    /// `Version` identifier, when stated.
107    pub version: Option<&'a str>,
108    /// `Publisher`: an `IfcActorSelect`, when stated.
109    pub publisher: Option<EntityId>,
110    /// `VersionDate` (IFC date-time string), when stated.
111    pub version_date: Option<&'a str>,
112    /// `Location` (URI) of the library, when stated.
113    pub location: Option<&'a str>,
114    /// `Description` of the library, when stated.
115    pub description: Option<&'a str>,
116}
117
118/// Draft for one IFC4 `IfcLibraryReference`.
119#[derive(Debug, Clone, Copy)]
120pub struct LibraryReferenceDraft<'a> {
121    /// `Location` (URI) of the reference; at least one of location/identification/name must be given.
122    pub location: Option<&'a str>,
123    /// `Identification` code within the library, when stated.
124    pub identification: Option<&'a str>,
125    /// `Name` of the referenced item, when stated.
126    pub name: Option<&'a str>,
127    /// `Description` of the reference, when stated.
128    pub description: Option<&'a str>,
129    /// `Language` of the referenced content, when stated.
130    pub language: Option<&'a str>,
131    /// `ReferencedLibrary`: an `IfcLibraryInformation`, when stated.
132    pub referenced_library: Option<EntityId>,
133}
134
135/// Draft shared by `IfcRelAssociatesClassification`/`Document`/`Library`.
136#[derive(Debug, Clone, Copy)]
137pub struct AssociationDraft<'a> {
138    /// Required `GlobalId`; must parse as a valid IFC GUID.
139    pub global_id: &'a str,
140    /// `Name` of the relationship, when stated.
141    pub name: Option<&'a str>,
142    /// `Description` of the relationship, when stated.
143    pub description: Option<&'a str>,
144    /// `RelatedObjects`: non-empty unique set of `IfcDefinitionSelect` ids.
145    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
269/// Validate and stage one `IfcClassification`; fails if `reference_tokens` is `Some` and empty.
270pub 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
301/// Validate and stage one `IfcClassificationReference`; fails if location/identification/name are all unstated or `referenced_source` does not resolve to an `IfcClassificationReferenceSelect`.
302pub 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
335/// Validate and stage one `IfcDocumentInformation`; fails on an invalid `Confidentiality`/`Status` enumerator, a `document_owner`/`editors` entry that is not an `IfcActorSelect`, or a duplicate/empty `editors` set.
336pub 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
408/// Validate and stage one `IfcDocumentReference`; fails if location/identification/name are all unstated, if `name` and `referenced_document` are not exactly one, or if `referenced_document` is not an `IfcDocumentInformation`.
409pub 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
448/// Validate and stage one `IfcLibraryInformation`; fails if `publisher` does not resolve to an `IfcActorSelect`.
449pub 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
470/// Validate and stage one `IfcLibraryReference`; fails if location/identification/name are all unstated or `referenced_library` is not an `IfcLibraryInformation`.
471pub 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
552/// Validate and stage an `IfcRelAssociatesClassification` linking `draft.related_objects` to `target`; fails if `target` is not an `IfcClassificationSelect`, or on any shared association precondition.
553pub 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}
569/// Validate and stage an `IfcRelAssociatesDocument` linking `draft.related_objects` to `target`; fails if `target` is not an `IfcDocumentSelect`, or on any shared association precondition.
570pub 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}
586/// Validate and stage an `IfcRelAssociatesLibrary` linking `draft.related_objects` to `target`; fails if `target` is not an `IfcLibrarySelect`, or on any shared association precondition.
587pub 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
604/// Stage an `IfcDocumentInformationRelationship`.
605///
606/// This is how a document says it supersedes, amends or accompanies
607/// another. `RelationshipType` is a free label rather than an enum:
608/// the schema does not fix the vocabulary, so it is written as given.
609///
610/// # Errors
611///
612/// Refuses a reference that is not an `IfcDocumentInformation`, an
613/// empty related set (`SET [1:?]`), and a document related to itself.
614pub 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        // A document that supersedes itself is a cycle at depth one.
634        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}