Skip to main content

openbim_step/
model.rs

1//! Generic, owned semantic data for a STEP physical file.
2//!
3//! The string storage type is generic. The default, [`Str`], is what
4//! [`parse`](crate::parse) produces: short values inline, longer ones
5//! shared. Borrowed APIs use `Cow<'a, str>`; applications may construct
6//! records with any string type.
7
8use std::fmt;
9use std::sync::Arc;
10
11/// An arbitrary-precision instance identifier as written by `#42`.
12///
13/// The Part 21 grammar does not impose a machine-integer bound, so the decimal
14/// digits are retained lexically.
15///
16/// Up to 22 digits -- every id a `u64` can hold -- are stored in the value
17/// itself; only longer ones allocate. A large file holds several
18/// ids per record, so this removes millions of allocations per parse.
19/// Comparison, hashing, ordering and `Debug` all go through [`Self::as_str`],
20/// so the representation is invisible.
21#[derive(Clone)]
22pub struct InstanceId(Digits);
23
24/// Digits stored inline; 22 keeps `InstanceId` at 24 bytes.
25const INLINE_DIGITS: usize = 22;
26
27#[derive(Clone)]
28enum Digits {
29    Inline { len: u8, bytes: [u8; INLINE_DIGITS] },
30    Heap(Box<str>),
31}
32
33impl InstanceId {
34    /// Creates an identifier from non-empty ASCII decimal digits.
35    #[must_use]
36    pub fn new(value: &str) -> Option<Self> {
37        Self::from_ascii_digits(value.as_bytes())
38    }
39
40    /// [`Self::new`] over bytes, for the parser: the lexer hands ids over as
41    /// bytes, and checking digits directly skips a separate UTF-8 pass.
42    pub(crate) fn from_ascii_digits(value: &[u8]) -> Option<Self> {
43        (!value.is_empty() && value.iter().all(u8::is_ascii_digit))
44            .then(|| Self::from_digits(value))
45    }
46
47    /// Stores already-validated ASCII digits.
48    pub(crate) fn from_digits(value: &[u8]) -> Self {
49        if value.len() <= INLINE_DIGITS {
50            let mut bytes = [0; INLINE_DIGITS];
51            bytes[..value.len()].copy_from_slice(value);
52            Self(Digits::Inline {
53                len: u8::try_from(value.len()).expect("INLINE_DIGITS fits in u8"),
54                bytes,
55            })
56        } else {
57            // ASCII digits are valid UTF-8, so this cannot fail.
58            let text = std::str::from_utf8(value).expect("ASCII digits are UTF-8");
59            Self(Digits::Heap(text.into()))
60        }
61    }
62
63    /// Returns the decimal digits without the leading `#`.
64    #[must_use]
65    pub fn as_str(&self) -> &str {
66        match &self.0 {
67            Digits::Inline { len, bytes } => {
68                // Only `from_digits` builds `Inline`, from validated ASCII
69                // digits, so this conversion cannot fail.
70                std::str::from_utf8(&bytes[..usize::from(*len)]).unwrap_or_default()
71            }
72            Digits::Heap(digits) => digits,
73        }
74    }
75}
76
77impl PartialEq for InstanceId {
78    fn eq(&self, other: &Self) -> bool {
79        self.as_str() == other.as_str()
80    }
81}
82
83impl Eq for InstanceId {}
84
85impl std::hash::Hash for InstanceId {
86    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
87        self.as_str().hash(state);
88    }
89}
90
91impl PartialOrd for InstanceId {
92    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
93        Some(self.cmp(other))
94    }
95}
96
97impl Ord for InstanceId {
98    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
99        self.as_str().cmp(other.as_str())
100    }
101}
102
103impl fmt::Debug for InstanceId {
104    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
105        formatter
106            .debug_tuple("InstanceId")
107            .field(&self.as_str())
108            .finish()
109    }
110}
111
112impl From<u64> for InstanceId {
113    fn from(value: u64) -> Self {
114        Self::from_digits(value.to_string().as_bytes())
115    }
116}
117
118impl fmt::Display for InstanceId {
119    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(formatter, "#{}", self.as_str())
121    }
122}
123
124/// The owned model's string: values of up to 22 bytes -- nearly every
125/// number, GUID and entity name in a STEP file -- are stored inline without
126/// an allocation, and longer ones are one shared `Arc<str>`.
127///
128/// [`parse`](crate::parse) hands out one allocation per distinct long name
129/// (record names, typed-value names, enumeration values) and shares it, so a
130/// file's millions of name uses cost a few hundred allocations. Cloning never
131/// copies the text. Equality, ordering and hashing are those of the `str`,
132/// so the representation is invisible.
133#[derive(Clone)]
134pub struct Str(StrRepr);
135
136/// Bytes stored inline; 22 keeps [`Str`] at 24 bytes, the size of a `String`.
137const INLINE_TEXT: usize = 22;
138
139#[derive(Clone)]
140enum StrRepr {
141    Inline { len: u8, bytes: [u8; INLINE_TEXT] },
142    Shared(Arc<str>),
143}
144
145impl Str {
146    /// The text.
147    #[must_use]
148    pub fn as_str(&self) -> &str {
149        match &self.0 {
150            // Only `Str::from` builds `Inline`, from a `str`, so the bytes
151            // are valid UTF-8 and this cannot fail.
152            StrRepr::Inline { len, bytes } => {
153                std::str::from_utf8(&bytes[..usize::from(*len)]).unwrap_or_default()
154            }
155            StrRepr::Shared(text) => text,
156        }
157    }
158
159    /// Whether this value shares its text with other clones rather than
160    /// storing it inline. Short values are never shared: copying them is
161    /// cheaper than an allocation.
162    #[must_use]
163    pub fn is_shared(&self) -> bool {
164        matches!(self.0, StrRepr::Shared(_))
165    }
166
167    /// Whether two values share one allocation.
168    #[must_use]
169    pub fn ptr_eq(&self, other: &Self) -> bool {
170        match (&self.0, &other.0) {
171            (StrRepr::Shared(a), StrRepr::Shared(b)) => Arc::ptr_eq(a, b),
172            _ => false,
173        }
174    }
175
176    /// Short text inline, `None` when it does not fit.
177    fn inline(text: &str) -> Option<Self> {
178        let mut bytes = [0; INLINE_TEXT];
179        bytes
180            .get_mut(..text.len())?
181            .copy_from_slice(text.as_bytes());
182        Some(Self(StrRepr::Inline {
183            len: u8::try_from(text.len()).expect("INLINE_TEXT fits in u8"),
184            bytes,
185        }))
186    }
187}
188
189impl From<&str> for Str {
190    fn from(text: &str) -> Self {
191        Self::inline(text).unwrap_or_else(|| Self(StrRepr::Shared(Arc::from(text))))
192    }
193}
194
195impl From<String> for Str {
196    fn from(text: String) -> Self {
197        Self::inline(&text).unwrap_or_else(|| Self(StrRepr::Shared(Arc::from(text))))
198    }
199}
200
201impl From<Arc<str>> for Str {
202    /// Keeps sharing a long text; a short one is stored inline.
203    fn from(text: Arc<str>) -> Self {
204        Self::inline(&text).unwrap_or(Self(StrRepr::Shared(text)))
205    }
206}
207
208impl From<&Str> for String {
209    fn from(text: &Str) -> Self {
210        text.as_str().to_owned()
211    }
212}
213
214impl std::ops::Deref for Str {
215    type Target = str;
216
217    fn deref(&self) -> &str {
218        self.as_str()
219    }
220}
221
222impl AsRef<str> for Str {
223    fn as_ref(&self) -> &str {
224        self.as_str()
225    }
226}
227
228impl std::borrow::Borrow<str> for Str {
229    fn borrow(&self) -> &str {
230        self.as_str()
231    }
232}
233
234impl PartialEq for Str {
235    fn eq(&self, other: &Self) -> bool {
236        self.as_str() == other.as_str()
237    }
238}
239
240impl Eq for Str {}
241
242impl PartialEq<str> for Str {
243    fn eq(&self, other: &str) -> bool {
244        self.as_str() == other
245    }
246}
247
248impl PartialEq<&str> for Str {
249    fn eq(&self, other: &&str) -> bool {
250        self.as_str() == *other
251    }
252}
253
254impl PartialOrd for Str {
255    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
256        Some(self.cmp(other))
257    }
258}
259
260impl Ord for Str {
261    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
262        self.as_str().cmp(other.as_str())
263    }
264}
265
266impl std::hash::Hash for Str {
267    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
268        self.as_str().hash(state);
269    }
270}
271
272impl fmt::Debug for Str {
273    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
274        fmt::Debug::fmt(self.as_str(), formatter)
275    }
276}
277
278impl fmt::Display for Str {
279    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
280        formatter.write_str(self.as_str())
281    }
282}
283
284impl Default for Str {
285    fn default() -> Self {
286        Self::from("")
287    }
288}
289
290/// One generic parameter value.
291#[derive(Debug, Clone, PartialEq)]
292pub enum Parameter<S = Str> {
293    /// `$`, an omitted value.
294    Null,
295    /// `*`, a derived value.
296    Derived,
297    /// `.T.` or `.F.`.
298    Bool(bool),
299    /// `.U.`, the third logical state.
300    LogicalUnknown,
301    /// Integer lexical form, preserved without a fixed precision limit.
302    Integer(S),
303    /// Real lexical form, preserved without a fixed precision limit.
304    Real(S),
305    /// Decoded text.
306    Text(S),
307    /// Binary digits without surrounding quotes.
308    Binary(S),
309    /// An enumeration name without surrounding dots.
310    Enum(S),
311    /// A `#` reference.
312    Ref(InstanceId),
313    /// A parenthesized aggregate. Boxed rather than a `Vec`: an aggregate
314    /// never grows after parsing, so it carries no capacity.
315    List(Box<[Self]>),
316    /// A named parameter wrapper.
317    Typed {
318        /// Wrapper name.
319        type_name: S,
320        /// Wrapped parameter. Multiple source arguments are represented by a
321        /// [`Parameter::List`].
322        value: Box<Self>,
323    },
324}
325
326impl<S> Parameter<S> {
327    /// Returns a referenced id when this is [`Parameter::Ref`].
328    #[must_use]
329    pub fn as_reference(&self) -> Option<InstanceId> {
330        match self {
331            Self::Ref(id) => Some(id.clone()),
332            _ => None,
333        }
334    }
335
336    /// Returns aggregate items when this is [`Parameter::List`].
337    #[must_use]
338    pub fn as_list(&self) -> Option<&[Self]> {
339        match self {
340            Self::List(items) => Some(items),
341            _ => None,
342        }
343    }
344
345    /// Recursively removes typed wrappers.
346    #[must_use]
347    pub fn unwrap_typed(&self) -> &Self {
348        match self {
349            Self::Typed { value, .. } => value.unwrap_typed(),
350            value => value,
351        }
352    }
353}
354
355impl<S: AsRef<str>> Parameter<S> {
356    /// Returns text content when this is [`Parameter::Text`].
357    #[must_use]
358    pub fn as_text(&self) -> Option<&str> {
359        match self {
360            Self::Text(text) => Some(text.as_ref()),
361            _ => None,
362        }
363    }
364
365    /// Returns a numeric approximation, accepting integer and real syntax.
366    #[must_use]
367    pub fn as_f64(&self) -> Option<f64> {
368        match self {
369            Self::Integer(value) | Self::Real(value) => value.as_ref().parse().ok(),
370            _ => None,
371        }
372    }
373}
374
375/// A record in `HEADER;`.
376#[derive(Debug, Clone, PartialEq)]
377pub struct HeaderRecord<S = Str> {
378    /// Record name.
379    pub name: S,
380    /// Positional parameters.
381    pub parameters: Box<[Parameter<S>]>,
382}
383
384/// One named parameter record within a DATA instance.
385#[derive(Debug, Clone, PartialEq)]
386pub struct Record<S = Str> {
387    /// Record name.
388    pub name: S,
389    /// Positional parameters.
390    pub parameters: Box<[Parameter<S>]>,
391}
392
393impl<S> Record<S> {
394    /// Creates a record.
395    #[must_use]
396    pub fn new(name: S, parameters: impl Into<Box<[Parameter<S>]>>) -> Self {
397        Self {
398            name,
399            parameters: parameters.into(),
400        }
401    }
402}
403
404/// The body of a `#id=...;` instance, as written.
405///
406/// Nearly every instance is simple, so a simple one holds its record inline
407/// rather than in a one-element list. A complex instance keeps its records
408/// in source order, and one written in complex form stays complex even when
409/// it holds a single record: `#1=(A(1));` writes back as written.
410#[derive(Debug, Clone, PartialEq)]
411pub enum Instance<S = Str> {
412    /// `NAME(...)`.
413    Simple(Record<S>),
414    /// `(A(...)B(...))`: the partial records of a complex instance.
415    Complex(Box<[Record<S>]>),
416}
417
418/// A simple or complex `#id=...;` instance in `DATA;`.
419#[derive(Debug, Clone, PartialEq)]
420pub struct DataRecord<S = Str> {
421    /// Source instance id.
422    pub id: InstanceId,
423    /// The instance body.
424    pub instance: Instance<S>,
425}
426
427impl<S> DataRecord<S> {
428    /// Creates a simple instance containing one named record.
429    #[must_use]
430    pub fn simple(id: InstanceId, name: S, parameters: impl Into<Box<[Parameter<S>]>>) -> Self {
431        Self {
432            id,
433            instance: Instance::Simple(Record::new(name, parameters)),
434        }
435    }
436
437    /// Creates a complex instance from its partial records.
438    #[must_use]
439    pub fn complex(id: InstanceId, records: impl Into<Box<[Record<S>]>>) -> Self {
440        Self {
441            id,
442            instance: Instance::Complex(records.into()),
443        }
444    }
445
446    /// The instance's records: one for a simple instance, the partial
447    /// records in source order for a complex one.
448    #[must_use]
449    pub fn records(&self) -> &[Record<S>] {
450        match &self.instance {
451            Instance::Simple(record) => std::slice::from_ref(record),
452            Instance::Complex(records) => records,
453        }
454    }
455
456    /// Returns the record of a simple instance.
457    #[must_use]
458    pub fn as_simple(&self) -> Option<&Record<S>> {
459        match &self.instance {
460            Instance::Simple(record) => Some(record),
461            Instance::Complex(_) => None,
462        }
463    }
464}
465
466/// The generic `HEADER; ... ENDSEC;` section.
467#[derive(Debug, Clone, Default, PartialEq)]
468pub struct HeaderSection<S = Str> {
469    /// Records in source order, including unknown extension records.
470    pub records: Vec<HeaderRecord<S>>,
471}
472
473/// Standard header fields projected from raw header records.
474///
475/// Every field is optional because the raw section may be incomplete. Calling
476/// this projection never removes or rewrites the underlying records.
477#[derive(Debug, Clone, Default, PartialEq, Eq)]
478pub struct StandardHeader {
479    /// `FILE_DESCRIPTION` descriptions.
480    pub description: Option<Vec<String>>,
481    /// `FILE_DESCRIPTION` implementation level.
482    pub implementation_level: Option<String>,
483    /// `FILE_NAME` source name.
484    pub name: Option<String>,
485    /// `FILE_NAME` timestamp.
486    pub time_stamp: Option<String>,
487    /// `FILE_NAME` authors.
488    pub author: Option<Vec<String>>,
489    /// `FILE_NAME` organizations.
490    pub organization: Option<Vec<String>>,
491    /// `FILE_NAME` preprocessor.
492    pub preprocessor_version: Option<String>,
493    /// `FILE_NAME` originating system.
494    pub originating_system: Option<String>,
495    /// `FILE_NAME` authorization.
496    pub authorization: Option<String>,
497    /// `FILE_SCHEMA` schema identifiers.
498    pub schema: Option<Vec<String>>,
499}
500
501impl<S: AsRef<str>> HeaderSection<S> {
502    /// Projects the three standard header records into named fields.
503    #[must_use]
504    pub fn standard(&self) -> StandardHeader {
505        let mut header = StandardHeader::default();
506        for record in &self.records {
507            match record.name.as_ref().to_ascii_uppercase().as_str() {
508                "FILE_DESCRIPTION" => {
509                    header.description = text_list(record.parameters.first());
510                    header.implementation_level = text(record.parameters.get(1));
511                }
512                "FILE_NAME" => {
513                    header.name = text(record.parameters.first());
514                    header.time_stamp = text(record.parameters.get(1));
515                    header.author = text_list(record.parameters.get(2));
516                    header.organization = text_list(record.parameters.get(3));
517                    header.preprocessor_version = text(record.parameters.get(4));
518                    header.originating_system = text(record.parameters.get(5));
519                    header.authorization = text(record.parameters.get(6));
520                }
521                "FILE_SCHEMA" => header.schema = text_list(record.parameters.first()),
522                _ => {}
523            }
524        }
525        header
526    }
527}
528
529fn text<S: AsRef<str>>(parameter: Option<&Parameter<S>>) -> Option<String> {
530    parameter?.as_text().map(ToOwned::to_owned)
531}
532
533fn text_list<S: AsRef<str>>(parameter: Option<&Parameter<S>>) -> Option<Vec<String>> {
534    Some(
535        parameter?
536            .as_list()?
537            .iter()
538            .filter_map(Parameter::as_text)
539            .map(ToOwned::to_owned)
540            .collect(),
541    )
542}
543
544/// The generic `DATA; ... ENDSEC;` section.
545#[derive(Debug, Clone, Default, PartialEq)]
546pub struct DataSection<S = Str> {
547    /// Records in source order.
548    pub records: Vec<DataRecord<S>>,
549}
550
551impl<S> DataSection<S> {
552    /// Finds a record by instance id.
553    #[must_use]
554    pub fn get(&self, id: &InstanceId) -> Option<&DataRecord<S>> {
555        self.records.iter().find(|record| &record.id == id)
556    }
557}
558
559/// A complete ISO 10303-21 exchange structure.
560#[derive(Debug, Clone, Default, PartialEq)]
561pub struct Exchange<S = Str> {
562    /// Header section.
563    pub header: HeaderSection<S>,
564    /// Data section.
565    pub data: DataSection<S>,
566}