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}