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, [`String`], is convenient
4//! for parsing owned exchanges; applications may construct records with an
5//! interned string type of their choice.
6
7use std::fmt;
8
9/// An arbitrary-precision instance identifier as written by `#42`.
10///
11/// The Part 21 grammar does not impose a machine-integer bound, so the decimal
12/// digits are retained lexically.
13#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct InstanceId(Box<str>);
15
16impl InstanceId {
17    /// Creates an identifier from non-empty ASCII decimal digits.
18    #[must_use]
19    pub fn new(value: &str) -> Option<Self> {
20        (!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit()))
21            .then(|| Self(value.into()))
22    }
23
24    /// Returns the decimal digits without the leading `#`.
25    #[must_use]
26    pub fn as_str(&self) -> &str {
27        &self.0
28    }
29}
30
31impl From<u64> for InstanceId {
32    fn from(value: u64) -> Self {
33        Self(value.to_string().into())
34    }
35}
36
37impl fmt::Display for InstanceId {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(formatter, "#{}", self.0)
40    }
41}
42
43/// One generic parameter value.
44#[derive(Debug, Clone, PartialEq)]
45pub enum Parameter<S = String> {
46    /// `$`, an omitted value.
47    Null,
48    /// `*`, a derived value.
49    Derived,
50    /// `.T.` or `.F.`.
51    Bool(bool),
52    /// `.U.`, the third logical state.
53    LogicalUnknown,
54    /// Integer lexical form, preserved without a fixed precision limit.
55    Integer(S),
56    /// Real lexical form, preserved without a fixed precision limit.
57    Real(S),
58    /// Decoded text.
59    Text(S),
60    /// Binary digits without surrounding quotes.
61    Binary(S),
62    /// An enumeration name without surrounding dots.
63    Enum(S),
64    /// A `#` reference.
65    Ref(InstanceId),
66    /// A parenthesized aggregate.
67    List(Vec<Self>),
68    /// A named parameter wrapper.
69    Typed {
70        /// Wrapper name.
71        type_name: S,
72        /// Wrapped parameter. Multiple source arguments are represented by a
73        /// [`Parameter::List`].
74        value: Box<Self>,
75    },
76}
77
78impl<S> Parameter<S> {
79    /// Returns a referenced id when this is [`Parameter::Ref`].
80    #[must_use]
81    pub fn as_reference(&self) -> Option<InstanceId> {
82        match self {
83            Self::Ref(id) => Some(id.clone()),
84            _ => None,
85        }
86    }
87
88    /// Returns aggregate items when this is [`Parameter::List`].
89    #[must_use]
90    pub fn as_list(&self) -> Option<&[Self]> {
91        match self {
92            Self::List(items) => Some(items),
93            _ => None,
94        }
95    }
96
97    /// Recursively removes typed wrappers.
98    #[must_use]
99    pub fn unwrap_typed(&self) -> &Self {
100        match self {
101            Self::Typed { value, .. } => value.unwrap_typed(),
102            value => value,
103        }
104    }
105}
106
107impl<S: AsRef<str>> Parameter<S> {
108    /// Returns text content when this is [`Parameter::Text`].
109    #[must_use]
110    pub fn as_text(&self) -> Option<&str> {
111        match self {
112            Self::Text(text) => Some(text.as_ref()),
113            _ => None,
114        }
115    }
116
117    /// Returns a numeric approximation, accepting integer and real syntax.
118    #[must_use]
119    pub fn as_f64(&self) -> Option<f64> {
120        match self {
121            Self::Integer(value) | Self::Real(value) => value.as_ref().parse().ok(),
122            _ => None,
123        }
124    }
125}
126
127/// A record in `HEADER;`.
128#[derive(Debug, Clone, PartialEq)]
129pub struct HeaderRecord<S = String> {
130    /// Record name.
131    pub name: S,
132    /// Positional parameters.
133    pub parameters: Vec<Parameter<S>>,
134}
135
136/// One named parameter record within a DATA instance.
137#[derive(Debug, Clone, PartialEq)]
138pub struct Record<S = String> {
139    /// Record name.
140    pub name: S,
141    /// Positional parameters.
142    pub parameters: Vec<Parameter<S>>,
143}
144
145/// A simple or complex `#id=...;` instance in `DATA;`.
146#[derive(Debug, Clone, PartialEq)]
147pub struct DataRecord<S = String> {
148    /// Source instance id.
149    pub id: InstanceId,
150    /// One record for a simple instance, multiple for a complex instance.
151    pub records: Vec<Record<S>>,
152}
153
154impl<S> DataRecord<S> {
155    /// Creates a simple instance containing one named record.
156    #[must_use]
157    pub fn simple(id: InstanceId, name: S, parameters: Vec<Parameter<S>>) -> Self {
158        Self {
159            id,
160            records: vec![Record { name, parameters }],
161        }
162    }
163
164    /// Returns the sole record of a simple instance.
165    #[must_use]
166    pub fn as_simple(&self) -> Option<&Record<S>> {
167        (self.records.len() == 1).then(|| &self.records[0])
168    }
169}
170
171/// The generic `HEADER; ... ENDSEC;` section.
172#[derive(Debug, Clone, Default, PartialEq)]
173pub struct HeaderSection<S = String> {
174    /// Records in source order, including unknown extension records.
175    pub records: Vec<HeaderRecord<S>>,
176}
177
178/// Standard header fields projected from raw header records.
179///
180/// Every field is optional because the raw section may be incomplete. Calling
181/// this projection never removes or rewrites the underlying records.
182#[derive(Debug, Clone, Default, PartialEq, Eq)]
183pub struct StandardHeader {
184    /// `FILE_DESCRIPTION` descriptions.
185    pub description: Option<Vec<String>>,
186    /// `FILE_DESCRIPTION` implementation level.
187    pub implementation_level: Option<String>,
188    /// `FILE_NAME` source name.
189    pub name: Option<String>,
190    /// `FILE_NAME` timestamp.
191    pub time_stamp: Option<String>,
192    /// `FILE_NAME` authors.
193    pub author: Option<Vec<String>>,
194    /// `FILE_NAME` organizations.
195    pub organization: Option<Vec<String>>,
196    /// `FILE_NAME` preprocessor.
197    pub preprocessor_version: Option<String>,
198    /// `FILE_NAME` originating system.
199    pub originating_system: Option<String>,
200    /// `FILE_NAME` authorization.
201    pub authorization: Option<String>,
202    /// `FILE_SCHEMA` schema identifiers.
203    pub schema: Option<Vec<String>>,
204}
205
206impl<S: AsRef<str>> HeaderSection<S> {
207    /// Projects the three standard header records into named fields.
208    #[must_use]
209    pub fn standard(&self) -> StandardHeader {
210        let mut header = StandardHeader::default();
211        for record in &self.records {
212            match record.name.as_ref().to_ascii_uppercase().as_str() {
213                "FILE_DESCRIPTION" => {
214                    header.description = text_list(record.parameters.first());
215                    header.implementation_level = text(record.parameters.get(1));
216                }
217                "FILE_NAME" => {
218                    header.name = text(record.parameters.first());
219                    header.time_stamp = text(record.parameters.get(1));
220                    header.author = text_list(record.parameters.get(2));
221                    header.organization = text_list(record.parameters.get(3));
222                    header.preprocessor_version = text(record.parameters.get(4));
223                    header.originating_system = text(record.parameters.get(5));
224                    header.authorization = text(record.parameters.get(6));
225                }
226                "FILE_SCHEMA" => header.schema = text_list(record.parameters.first()),
227                _ => {}
228            }
229        }
230        header
231    }
232}
233
234fn text<S: AsRef<str>>(parameter: Option<&Parameter<S>>) -> Option<String> {
235    parameter?.as_text().map(ToOwned::to_owned)
236}
237
238fn text_list<S: AsRef<str>>(parameter: Option<&Parameter<S>>) -> Option<Vec<String>> {
239    Some(
240        parameter?
241            .as_list()?
242            .iter()
243            .filter_map(Parameter::as_text)
244            .map(ToOwned::to_owned)
245            .collect(),
246    )
247}
248
249/// The generic `DATA; ... ENDSEC;` section.
250#[derive(Debug, Clone, Default, PartialEq)]
251pub struct DataSection<S = String> {
252    /// Records in source order.
253    pub records: Vec<DataRecord<S>>,
254}
255
256impl<S> DataSection<S> {
257    /// Finds a record by instance id.
258    #[must_use]
259    pub fn get(&self, id: &InstanceId) -> Option<&DataRecord<S>> {
260        self.records.iter().find(|record| &record.id == id)
261    }
262}
263
264/// A complete ISO 10303-21 exchange structure.
265#[derive(Debug, Clone, Default, PartialEq)]
266pub struct Exchange<S = String> {
267    /// Header section.
268    pub header: HeaderSection<S>,
269    /// Data section.
270    pub data: DataSection<S>,
271}