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}