Skip to main content

ifc_xml/
codec.rs

1//! Codec state and the shared model-codec adapter.
2
3use crate::{reader, writer, XmlProfile};
4use ifc_model::{Codec, Model, ModelError};
5
6/// Which XML layout a codec reads and writes.
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum XmlLayout {
10    /// This crate's own lossless layout: one top-level element per entity,
11    /// `i<n>` ids and references, explicit `kind` markers where an attribute
12    /// string could not carry a value's kind. Reads and writes.
13    #[default]
14    Native,
15    /// The buildingSMART ifcXML configuration of ISO 10303-28 that the
16    /// release XSD declares: entities nested and defined in place, `ref` /
17    /// `href` references, inverse attributes, `-wrapper` typed values and
18    /// space-separated list attributes. Reads and writes, always
19    /// schema-strict; written documents validate against the release XSD.
20    Xsd,
21}
22
23/// How a native-layout codec with a schema treats content the schema does
24/// not declare.
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum SchemaReading {
28    /// Every value is typed from its attribute's declaration, never from its
29    /// text, and an entity, attribute or value the schema does not declare
30    /// is a typed error naming it. The default with a schema.
31    #[default]
32    Strict,
33    /// The pre-0.4 behaviour: names resolve through the schema when they
34    /// can, values are inferred from their text, and unknown names are kept
35    /// after the declared slots. Round-trips models the schema does not
36    /// describe, at the price of reading `Name="1"` as an integer.
37    Lenient,
38}
39
40/// The ifcXML codec.
41///
42/// Construct with [`XmlCodec::default`] for the lossless compatibility dialect,
43/// [`XmlCodec::strict`] for an exact namespace/release profile,
44/// [`XmlCodec::with_schema_and_profile`] for strict output with schema-correct
45/// attribute names, or [`XmlCodec::xsd`] to read and write the buildingSMART
46/// XSD configuration.
47#[derive(Debug, Clone, Default)]
48pub struct XmlCodec {
49    profile: Option<XmlProfile>,
50    layout: XmlLayout,
51    reading: SchemaReading,
52    #[cfg(feature = "schema")]
53    schema: Option<std::sync::Arc<ifc_schema::Schema>>,
54}
55
56impl XmlCodec {
57    /// A codec that enforces one exact ifcXML release namespace and schema token.
58    ///
59    /// It writes and reads the native layout under that namespace. The
60    /// output is well-formed and its root is the XSD's `ifcXML` element, but
61    /// it is not valid against the release XSD: see [`XmlProfile`].
62    #[must_use]
63    pub const fn strict(profile: XmlProfile) -> Self {
64        Self {
65            profile: Some(profile),
66            layout: XmlLayout::Native,
67            reading: SchemaReading::Strict,
68            #[cfg(feature = "schema")]
69            schema: None,
70        }
71    }
72
73    /// The strict release profile, or `None` for compatibility mode.
74    #[must_use]
75    pub const fn profile(&self) -> Option<XmlProfile> {
76        self.profile
77    }
78
79    /// The XML layout this codec reads and writes.
80    #[must_use]
81    pub const fn layout(&self) -> XmlLayout {
82        self.layout
83    }
84
85    /// How content the schema does not declare is treated. Applies to the
86    /// native layout with a schema; the XSD layout is always strict.
87    #[must_use]
88    pub const fn reading(&self) -> SchemaReading {
89        match self.layout {
90            XmlLayout::Xsd => SchemaReading::Strict,
91            XmlLayout::Native => self.reading,
92        }
93    }
94
95    /// The same codec reading native documents with `reading`.
96    ///
97    /// [`SchemaReading::Lenient`] restores the pre-0.4 schema-aware read.
98    /// Has no effect on an [`XmlLayout::Xsd`] codec, which has no lenient
99    /// mode.
100    #[must_use]
101    pub const fn with_reading(mut self, reading: SchemaReading) -> Self {
102        self.reading = reading;
103        self
104    }
105
106    /// A codec that emits schema-correct attribute names and reads strictly.
107    ///
108    /// Reading types every value from its declaration and refuses names the
109    /// schema does not declare ([`SchemaReading::Strict`]); chain
110    /// [`Self::with_reading`] for the lenient read.
111    #[cfg(feature = "schema")]
112    #[must_use]
113    pub fn with_schema(schema: std::sync::Arc<ifc_schema::Schema>) -> Self {
114        Self {
115            profile: None,
116            layout: XmlLayout::Native,
117            reading: SchemaReading::Strict,
118            schema: Some(schema),
119        }
120    }
121
122    /// A strict release-profile codec with schema-backed attribute names.
123    ///
124    /// Reads strictly, as [`Self::with_schema`] does. Like [`Self::strict`],
125    /// it writes the native layout, not the XSD configuration.
126    #[cfg(feature = "schema")]
127    #[must_use]
128    pub fn with_schema_and_profile(
129        schema: std::sync::Arc<ifc_schema::Schema>,
130        profile: XmlProfile,
131    ) -> Self {
132        Self {
133            profile: Some(profile),
134            layout: XmlLayout::Native,
135            reading: SchemaReading::Strict,
136            schema: Some(schema),
137        }
138    }
139
140    /// A reader and writer of the buildingSMART XSD configuration of
141    /// `profile`'s release.
142    ///
143    /// `schema` must be that release's schema; a mismatch is refused when
144    /// reading or writing. A document reads into the same [`Model`] as its
145    /// STEP form. A model writes as a document that validates against the
146    /// release XSD and reads back to the same model, its entities numbered
147    /// in model order; what the configuration cannot carry exactly is
148    /// refused with [`crate::XmlError::Unrepresentable`] or another typed
149    /// error, never written differently. The model's header must declare
150    /// the profile's schema token.
151    #[cfg(feature = "schema")]
152    #[must_use]
153    pub fn xsd(schema: std::sync::Arc<ifc_schema::Schema>, profile: XmlProfile) -> Self {
154        Self {
155            profile: Some(profile),
156            layout: XmlLayout::Xsd,
157            reading: SchemaReading::Strict,
158            schema: Some(schema),
159        }
160    }
161
162    /// The schema in use, if any.
163    #[cfg(feature = "schema")]
164    #[must_use]
165    pub fn schema(&self) -> Option<&ifc_schema::Schema> {
166        self.schema.as_deref()
167    }
168
169    /// The schema a native read types values from, when reading strictly.
170    #[cfg(feature = "schema")]
171    pub(crate) fn strict_schema(&self) -> Option<&ifc_schema::Schema> {
172        match self.reading {
173            SchemaReading::Strict => self.schema(),
174            SchemaReading::Lenient => None,
175        }
176    }
177}
178
179impl Codec for XmlCodec {
180    fn name(&self) -> &'static str {
181        "ifcXML"
182    }
183
184    fn extensions(&self) -> &'static [&'static str] {
185        &["ifcxml", "xml"]
186    }
187
188    fn detect(&self, bytes: &[u8]) -> bool {
189        reader::looks_like_xml(bytes)
190    }
191
192    fn read_bytes(&self, bytes: &[u8]) -> Result<Model, ModelError> {
193        reader::read(self, bytes).map_err(|error| ModelError::Syntax {
194            offset: 0,
195            detail: error.to_string(),
196        })
197    }
198
199    fn write(&self, model: &Model, out: &mut dyn std::io::Write) -> Result<(), ModelError> {
200        let bytes =
201            writer::write(self, model).map_err(|error| ModelError::Write(error.to_string()))?;
202        out.write_all(&bytes)
203            .map_err(|error| ModelError::Io(error.to_string()))
204    }
205}