Skip to main content

openbim_ids/
model.rs

1//! The typed content of an IDS 1.0 document.
2//!
3//! These types mirror `ids.xsd` 1.0.0 closely enough that every schema-valid
4//! document has exactly one representation, and nothing the schema leaves to
5//! the author is decided here: a value that the schema types as `xs:string`
6//! stays the string the document wrote, and a restriction keeps its facets
7//! rather than being compiled into a matcher. Matching values against a model
8//! is auditing, which belongs to a later layer.
9//!
10//! What the reader *does* settle is everything the schema itself settles:
11//! attribute defaults are applied ([`Occurrence::DEFAULT`] for a requirement
12//! facet without `@cardinality`, `1` for an absent `minOccurs`/`maxOccurs`),
13//! enumerated tokens become enums, and list-typed attributes are split.
14
15use core::fmt;
16
17use openbim_core::Detected;
18
19use crate::{IdsVersion, Occurrence};
20
21/// A whole IDS document.
22#[derive(Debug, Clone, PartialEq)]
23pub struct Ids {
24    /// Which IDS revision the document is, and how that was established.
25    ///
26    /// Always resolves to [`IdsVersion::Ids1_0`] for a document the reader
27    /// accepted; [`Detected::Inferred`] means the document did not say so and
28    /// was read as 1.0 because its shape is 1.0 (see
29    /// [`read`](crate::read)).
30    pub version: Detected<IdsVersion>,
31    /// Document metadata.
32    pub info: Info,
33    /// The specifications, in document order. The schema requires at least one.
34    pub specifications: Vec<Specification>,
35}
36
37impl Ids {
38    /// A new IDS 1.0 document with no specifications yet.
39    ///
40    /// The version is [`Detected::Declared`]: a producer states the revision
41    /// it writes, so there is no detection evidence to fake. Push at least one
42    /// [`Specification`] before [writing](mod@crate::write).
43    #[must_use]
44    pub fn new(info: Info) -> Ids {
45        Ids {
46            version: Detected::Declared(IdsVersion::Ids1_0),
47            info,
48            specifications: Vec::new(),
49        }
50    }
51}
52
53/// The `<info>` block. Only `title` is required by the schema.
54#[derive(Debug, Clone, Default, PartialEq, Eq)]
55pub struct Info {
56    /// `<title>`.
57    pub title: String,
58    /// `<copyright>`.
59    pub copyright: Option<String>,
60    /// `<version>`, the document's own version, not the IDS revision.
61    pub version: Option<String>,
62    /// `<description>`.
63    pub description: Option<String>,
64    /// `<author>`, which the schema constrains to look like an e-mail address.
65    pub author: Option<String>,
66    /// `<date>`, an `xs:date` in its lexical form, e.g. `2024-06-01`.
67    pub date: Option<String>,
68    /// `<purpose>`.
69    pub purpose: Option<String>,
70    /// `<milestone>`.
71    pub milestone: Option<String>,
72}
73
74impl Info {
75    /// Metadata with a title and nothing else.
76    #[must_use]
77    pub fn new(title: impl Into<String>) -> Info {
78        Info {
79            title: title.into(),
80            ..Info::default()
81        }
82    }
83}
84
85/// One `<specification>`.
86#[derive(Debug, Clone, PartialEq)]
87pub struct Specification {
88    /// `@name`, required.
89    pub name: String,
90    /// `@ifcVersion`, an `xs:list`: `"IFC2X3 IFC4"` is two releases.
91    pub ifc_versions: Vec<IfcVersion>,
92    /// `@identifier`. Not unique across documents, by the schema's own note.
93    pub identifier: Option<String>,
94    /// `@description`.
95    pub description: Option<String>,
96    /// `@instructions`, meant for the model author.
97    pub instructions: Option<String>,
98    /// Which objects the specification is about.
99    pub applicability: Applicability,
100    /// What those objects must satisfy. `None` when `<requirements>` is
101    /// absent, which is distinct from an empty `<requirements/>` only in the
102    /// description it can carry.
103    pub requirements: Option<Requirements>,
104}
105
106impl Specification {
107    /// A specification of the given IFC releases with an empty, optional
108    /// applicability and no requirements.
109    ///
110    /// Optional (`minOccurs="0" maxOccurs="unbounded"`) means the
111    /// requirements apply to whatever objects match, and a model with none
112    /// passes. Use [`Applicability::set_occurrence`] to demand or prohibit
113    /// applicable objects. Add at least one applicability facet before
114    /// [writing](mod@crate::write).
115    #[must_use]
116    pub fn new(
117        name: impl Into<String>,
118        ifc_versions: impl IntoIterator<Item = IfcVersion>,
119    ) -> Self {
120        Specification {
121            name: name.into(),
122            ifc_versions: ifc_versions.into_iter().collect(),
123            identifier: None,
124            description: None,
125            instructions: None,
126            applicability: Applicability::new(Occurrence::Optional),
127            requirements: None,
128        }
129    }
130
131    /// Appends a requirement, creating `<requirements>` if absent.
132    pub fn require(&mut self, requirement: impl Into<Requirement>) -> &mut Self {
133        self.requirements
134            .get_or_insert_with(Requirements::default)
135            .facets
136            .push(requirement.into());
137        self
138    }
139
140    /// How many applicable objects the specification demands.
141    ///
142    /// Lowered from the applicability's `minOccurs`/`maxOccurs`: `0..0` is
143    /// prohibited, `0..n` optional, anything with a non-zero minimum required.
144    #[must_use]
145    pub fn occurrence(&self) -> Occurrence {
146        self.applicability.occurrence()
147    }
148}
149
150/// A release named in `@ifcVersion`.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
152pub enum IfcVersion {
153    /// `IFC2X3`.
154    Ifc2x3,
155    /// `IFC4`.
156    Ifc4,
157    /// `IFC4X3_ADD2`.
158    Ifc4x3Add2,
159}
160
161impl IfcVersion {
162    /// Every release the schema enumerates.
163    pub const ALL: [IfcVersion; 3] = [IfcVersion::Ifc2x3, IfcVersion::Ifc4, IfcVersion::Ifc4x3Add2];
164
165    /// The token as the schema spells it.
166    #[must_use]
167    pub fn as_str(self) -> &'static str {
168        match self {
169            IfcVersion::Ifc2x3 => "IFC2X3",
170            IfcVersion::Ifc4 => "IFC4",
171            IfcVersion::Ifc4x3Add2 => "IFC4X3_ADD2",
172        }
173    }
174
175    /// Parses one schema token. Case-sensitive, as the enumeration is.
176    #[must_use]
177    pub fn from_token(token: &str) -> Option<IfcVersion> {
178        IfcVersion::ALL.into_iter().find(|v| v.as_str() == token)
179    }
180}
181
182impl fmt::Display for IfcVersion {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        f.write_str(self.as_str())
185    }
186}
187
188/// `<applicability>`: the facets an object must match to be checked.
189#[derive(Debug, Clone, PartialEq)]
190pub struct Applicability {
191    /// `@minOccurs`, default `1`.
192    pub min_occurs: u32,
193    /// `@maxOccurs`, default `1`; `None` is `unbounded`.
194    pub max_occurs: Option<u32>,
195    /// The facets, in document order. An object is applicable when it
196    /// matches all of them.
197    pub facets: Vec<Facet>,
198}
199
200impl Applicability {
201    /// An applicability without facets and with the bounds `occurrence`
202    /// spells, see [`Applicability::set_occurrence`].
203    #[must_use]
204    pub fn new(occurrence: Occurrence) -> Applicability {
205        let mut applicability = Applicability {
206            min_occurs: 1,
207            max_occurs: None,
208            facets: Vec::new(),
209        };
210        applicability.set_occurrence(occurrence);
211        applicability
212    }
213
214    /// Sets the bounds to spell `occurrence`: required is `1..unbounded`,
215    /// optional `0..unbounded`, prohibited `0..0`.
216    pub fn set_occurrence(&mut self, occurrence: Occurrence) -> &mut Self {
217        (self.min_occurs, self.max_occurs) = match occurrence {
218            Occurrence::Required => (1, None),
219            Occurrence::Optional => (0, None),
220            Occurrence::Prohibited => (0, Some(0)),
221        };
222        self
223    }
224
225    /// The occurrence the bounds spell, see [`Specification::occurrence`].
226    #[must_use]
227    pub fn occurrence(&self) -> Occurrence {
228        Occurrence::from_xs_occurs(self.min_occurs, self.max_occurs)
229    }
230}
231
232/// `<requirements>`.
233#[derive(Debug, Clone, Default, PartialEq)]
234pub struct Requirements {
235    /// `@description`.
236    pub description: Option<String>,
237    /// The requirement facets in document order. The schema lets facet kinds
238    /// interleave, so the order is the document's, not grouped by kind.
239    pub facets: Vec<Requirement>,
240}
241
242/// One requirement facet with the attributes only requirements carry.
243#[derive(Debug, Clone, PartialEq)]
244pub struct Requirement {
245    /// What is required.
246    pub facet: Facet,
247    /// `@cardinality`, [`Occurrence::DEFAULT`] when absent. An entity facet
248    /// has no cardinality and is always required; a `partOf` facet cannot be
249    /// optional.
250    pub occurrence: Occurrence,
251    /// `@uri`, allowed on classification, property and material facets.
252    pub uri: Option<String>,
253    /// `@instructions`, meant for the model author.
254    pub instructions: Option<String>,
255}
256
257impl Requirement {
258    /// A required facet with no `uri` or `instructions`.
259    #[must_use]
260    pub fn new(facet: impl Into<Facet>) -> Requirement {
261        Requirement::with_occurrence(facet, Occurrence::Required)
262    }
263
264    /// A facet with the given cardinality and no `uri` or `instructions`.
265    #[must_use]
266    pub fn with_occurrence(facet: impl Into<Facet>, occurrence: Occurrence) -> Requirement {
267        Requirement {
268            facet: facet.into(),
269            occurrence,
270            uri: None,
271            instructions: None,
272        }
273    }
274}
275
276impl<F: Into<Facet>> From<F> for Requirement {
277    fn from(facet: F) -> Requirement {
278        Requirement::new(facet)
279    }
280}
281
282/// A facet: one condition on an object.
283#[derive(Debug, Clone, PartialEq)]
284pub enum Facet {
285    /// `<entity>`: the object's IFC class and predefined type.
286    Entity(Entity),
287    /// `<partOf>`: a relationship to another object.
288    PartOf(PartOf),
289    /// `<classification>`: a classification reference.
290    Classification(Classification),
291    /// `<attribute>`: a direct IFC attribute.
292    Attribute(Attribute),
293    /// `<property>`: a property in a property or quantity set.
294    Property(Property),
295    /// `<material>`: an assigned material.
296    Material(Material),
297}
298
299macro_rules! facet_from {
300    ($($kind:ident),*) => {$(
301        impl From<$kind> for Facet {
302            fn from(facet: $kind) -> Facet {
303                Facet::$kind(facet)
304            }
305        }
306    )*};
307}
308
309facet_from!(
310    Entity,
311    PartOf,
312    Classification,
313    Attribute,
314    Property,
315    Material
316);
317
318impl Facet {
319    /// The element name of the facet, e.g. `"partOf"`.
320    #[must_use]
321    pub fn kind(&self) -> &'static str {
322        match self {
323            Facet::Entity(_) => "entity",
324            Facet::PartOf(_) => "partOf",
325            Facet::Classification(_) => "classification",
326            Facet::Attribute(_) => "attribute",
327            Facet::Property(_) => "property",
328            Facet::Material(_) => "material",
329        }
330    }
331}
332
333/// `<entity>`.
334#[derive(Debug, Clone, PartialEq)]
335pub struct Entity {
336    /// `<name>`, an upper-case IFC class name such as `IFCWALL`.
337    pub name: Value,
338    /// `<predefinedType>`.
339    pub predefined_type: Option<Value>,
340}
341
342impl Entity {
343    /// An entity facet without a predefined type.
344    #[must_use]
345    pub fn new(name: impl Into<Value>) -> Entity {
346        Entity {
347            name: name.into(),
348            predefined_type: None,
349        }
350    }
351}
352
353/// `<partOf>`.
354#[derive(Debug, Clone, PartialEq)]
355pub struct PartOf {
356    /// The entity at the other end of the relationship.
357    pub entity: Entity,
358    /// `@relation`; `None` means any of them.
359    pub relation: Option<Relation>,
360}
361
362impl PartOf {
363    /// A `partOf` facet following any relation.
364    #[must_use]
365    pub fn new(entity: Entity) -> PartOf {
366        PartOf {
367            entity,
368            relation: None,
369        }
370    }
371}
372
373/// A relationship a `partOf` facet can follow.
374#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
375pub enum Relation {
376    /// `IFCRELAGGREGATES`.
377    Aggregates,
378    /// `IFCRELASSIGNSTOGROUP`.
379    AssignsToGroup,
380    /// `IFCRELCONTAINEDINSPATIALSTRUCTURE`.
381    ContainedInSpatialStructure,
382    /// `IFCRELNESTS`.
383    Nests,
384    /// `IFCRELVOIDSELEMENT IFCRELFILLSELEMENT`: an element filling an opening
385    /// that voids another element. One token that contains a space.
386    VoidsElementFillsElement,
387}
388
389impl Relation {
390    /// Every relation the schema enumerates.
391    pub const ALL: [Relation; 5] = [
392        Relation::Aggregates,
393        Relation::AssignsToGroup,
394        Relation::ContainedInSpatialStructure,
395        Relation::Nests,
396        Relation::VoidsElementFillsElement,
397    ];
398
399    /// The token as the schema spells it.
400    #[must_use]
401    pub fn as_str(self) -> &'static str {
402        match self {
403            Relation::Aggregates => "IFCRELAGGREGATES",
404            Relation::AssignsToGroup => "IFCRELASSIGNSTOGROUP",
405            Relation::ContainedInSpatialStructure => "IFCRELCONTAINEDINSPATIALSTRUCTURE",
406            Relation::Nests => "IFCRELNESTS",
407            Relation::VoidsElementFillsElement => "IFCRELVOIDSELEMENT IFCRELFILLSELEMENT",
408        }
409    }
410
411    /// Parses one schema token, exactly as the enumeration spells it.
412    #[must_use]
413    pub fn from_token(token: &str) -> Option<Relation> {
414        Relation::ALL.into_iter().find(|r| r.as_str() == token)
415    }
416}
417
418impl fmt::Display for Relation {
419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420        f.write_str(self.as_str())
421    }
422}
423
424/// `<classification>`.
425#[derive(Debug, Clone, PartialEq)]
426pub struct Classification {
427    /// `<value>`, the reference's identification. `None` means any.
428    pub value: Option<Value>,
429    /// `<system>`, the classification system's name. Required in 1.0.
430    pub system: Value,
431}
432
433impl Classification {
434    /// Any reference in the given system.
435    #[must_use]
436    pub fn new(system: impl Into<Value>) -> Classification {
437        Classification {
438            value: None,
439            system: system.into(),
440        }
441    }
442}
443
444/// `<attribute>`.
445#[derive(Debug, Clone, PartialEq)]
446pub struct Attribute {
447    /// `<name>`, the attribute name as the IFC schema spells it, e.g. `Name`.
448    pub name: Value,
449    /// `<value>`. `None` means any non-empty value.
450    pub value: Option<Value>,
451}
452
453impl Attribute {
454    /// The attribute with any non-empty value.
455    #[must_use]
456    pub fn new(name: impl Into<Value>) -> Attribute {
457        Attribute {
458            name: name.into(),
459            value: None,
460        }
461    }
462}
463
464/// `<property>`.
465#[derive(Debug, Clone, PartialEq)]
466pub struct Property {
467    /// `<propertySet>`, e.g. `Pset_WallCommon`.
468    pub property_set: Value,
469    /// `<baseName>`, the property name as stored in the file.
470    pub base_name: Value,
471    /// `<value>`. `None` means any non-empty value.
472    pub value: Option<Value>,
473    /// `@dataType`, an upper-case IFC defined type such as `IFCLABEL`.
474    pub data_type: Option<String>,
475}
476
477impl Property {
478    /// The property with any non-empty value and no data type.
479    #[must_use]
480    pub fn new(property_set: impl Into<Value>, base_name: impl Into<Value>) -> Property {
481        Property {
482            property_set: property_set.into(),
483            base_name: base_name.into(),
484            value: None,
485            data_type: None,
486        }
487    }
488}
489
490/// `<material>`. [`Material::default`] is any material.
491#[derive(Debug, Clone, Default, PartialEq)]
492pub struct Material {
493    /// `<value>`, a material or material category name. `None` means any.
494    pub value: Option<Value>,
495}
496
497/// An `ids:idsValue`: a literal or an XML Schema restriction.
498#[derive(Debug, Clone, PartialEq)]
499pub enum Value {
500    /// `<simpleValue>`, verbatim. An `xs:string`, so surrounding whitespace
501    /// is part of the value.
502    Simple(String),
503    /// `<xs:restriction>`.
504    Restriction(Box<Restriction>),
505}
506
507impl From<&str> for Value {
508    fn from(value: &str) -> Value {
509        Value::Simple(value.to_owned())
510    }
511}
512
513impl From<String> for Value {
514    fn from(value: String) -> Value {
515        Value::Simple(value)
516    }
517}
518
519impl From<Restriction> for Value {
520    fn from(restriction: Restriction) -> Value {
521        Value::Restriction(Box::new(restriction))
522    }
523}
524
525impl Value {
526    /// The literal, when this is a `<simpleValue>`.
527    #[must_use]
528    pub fn as_simple(&self) -> Option<&str> {
529        match self {
530            Value::Simple(value) => Some(value),
531            Value::Restriction(_) => None,
532        }
533    }
534}
535
536/// An `<xs:restriction>` and its constraining facets.
537///
538/// Facet values stay in the lexical form the document wrote: whether
539/// `"2.50"` is a number depends on `base` and on what it is compared with,
540/// which is an audit decision.
541///
542/// Within one restriction, several `enumeration`s are alternatives and so
543/// are several `pattern`s, as in XML Schema.
544#[derive(Debug, Clone, Default, PartialEq, Eq)]
545pub struct Restriction {
546    /// The local name of the XML Schema built-in type in `@base`, e.g.
547    /// `string` for `xs:string`. The prefix is resolved, so `xsd:string` is
548    /// `string` too.
549    pub base: String,
550    /// `<xs:enumeration>` values.
551    pub enumeration: Vec<String>,
552    /// `<xs:pattern>` values: XML Schema regular expressions, which match the
553    /// whole value (they are implicitly anchored).
554    pub patterns: Vec<String>,
555    /// `<xs:minInclusive>`.
556    pub min_inclusive: Option<String>,
557    /// `<xs:maxInclusive>`.
558    pub max_inclusive: Option<String>,
559    /// `<xs:minExclusive>`.
560    pub min_exclusive: Option<String>,
561    /// `<xs:maxExclusive>`.
562    pub max_exclusive: Option<String>,
563    /// `<xs:length>`.
564    pub length: Option<u64>,
565    /// `<xs:minLength>`.
566    pub min_length: Option<u64>,
567    /// `<xs:maxLength>`.
568    pub max_length: Option<u64>,
569    /// `<xs:totalDigits>`.
570    pub total_digits: Option<u64>,
571    /// `<xs:fractionDigits>`.
572    pub fraction_digits: Option<u64>,
573}
574
575impl Restriction {
576    /// A restriction of `base` without facets, e.g. `Restriction::new("string")`.
577    ///
578    /// Add at least one facet before [writing](mod@crate::write); a restriction
579    /// without facets is refused.
580    #[must_use]
581    pub fn new(base: impl Into<String>) -> Restriction {
582        Restriction {
583            base: base.into(),
584            ..Restriction::default()
585        }
586    }
587
588    /// An `xs:string` restriction to one of `values`.
589    #[must_use]
590    pub fn enumeration<I, S>(values: I) -> Restriction
591    where
592        I: IntoIterator<Item = S>,
593        S: Into<String>,
594    {
595        Restriction {
596            enumeration: values.into_iter().map(Into::into).collect(),
597            ..Restriction::new("string")
598        }
599    }
600
601    /// An `xs:string` restriction to an XML Schema regular expression.
602    #[must_use]
603    pub fn pattern(pattern: impl Into<String>) -> Restriction {
604        Restriction {
605            patterns: vec![pattern.into()],
606            ..Restriction::new("string")
607        }
608    }
609}