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}