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