Skip to main content

aws_smithy_xml/
codec.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! XML codec implementation for schema-based serialization.
7//!
8//! Provides [`XmlCodec`] — a [`Codec`] producing the [`XmlSerializer`] /
9//! [`XmlDeserializer`] pair used by the AWS REST XML protocol. Per-protocol
10//! behavior (default timestamp format, etc.) is configured via
11//! [`XmlCodecSettings`].
12
13use aws_smithy_schema::codec::Codec;
14use aws_smithy_types::date_time::Format as TimestampFormat;
15use std::sync::Arc;
16
17mod deserializer;
18mod serializer;
19
20pub use deserializer::{find_depth2_element_slice_by, XmlDeserializer};
21pub use serializer::XmlSerializer;
22
23/// Configuration for XML codec behavior.
24///
25/// Use the builder methods to construct settings:
26/// ```
27/// use aws_smithy_xml::codec::XmlCodecSettings;
28///
29/// let settings = XmlCodecSettings::builder().build();
30/// ```
31#[derive(Debug)]
32pub struct XmlCodecSettings {
33    default_timestamp_format: TimestampFormat,
34    max_depth: u32,
35    validate_document: bool,
36    validate_root_name: bool,
37    strict_collection_element_names: bool,
38    error_root_name: Option<&'static str>,
39}
40
41impl XmlCodecSettings {
42    /// Creates a builder for `XmlCodecSettings`.
43    pub fn builder() -> XmlCodecSettingsBuilder {
44        XmlCodecSettingsBuilder::default()
45    }
46
47    /// Default timestamp format when not specified by `@timestampFormat` trait.
48    /// REST XML uses `date-time`.
49    pub fn default_timestamp_format(&self) -> TimestampFormat {
50        self.default_timestamp_format
51    }
52
53    /// Maximum aggregate nesting depth the deserializer will accept before
54    /// returning an error. Defends against stack overflow on recursive
55    /// shapes and deeply-nested XML payloads.
56    pub fn max_depth(&self) -> u32 {
57        self.max_depth
58    }
59
60    /// Whether a wrapped list or map reads only the children named as its
61    /// items or entries. See
62    /// [`XmlCodecSettingsBuilder::strict_collection_element_names`].
63    pub fn strict_collection_element_names(&self) -> bool {
64        self.strict_collection_element_names
65    }
66}
67
68impl Default for XmlCodecSettings {
69    fn default() -> Self {
70        Self {
71            default_timestamp_format: TimestampFormat::DateTime,
72            max_depth: crate::codec::deserializer::MAX_DESERIALIZE_DEPTH,
73            validate_document: false,
74            validate_root_name: false,
75            strict_collection_element_names: false,
76            error_root_name: None,
77        }
78    }
79}
80
81/// Builder for [`XmlCodecSettings`].
82#[derive(Debug, Clone)]
83pub struct XmlCodecSettingsBuilder {
84    default_timestamp_format: TimestampFormat,
85    max_depth: u32,
86    validate_document: bool,
87    validate_root_name: bool,
88    strict_collection_element_names: bool,
89    error_root_name: Option<&'static str>,
90}
91
92impl Default for XmlCodecSettingsBuilder {
93    fn default() -> Self {
94        Self {
95            default_timestamp_format: TimestampFormat::DateTime,
96            max_depth: crate::codec::deserializer::MAX_DESERIALIZE_DEPTH,
97            validate_document: false,
98            validate_root_name: false,
99            strict_collection_element_names: false,
100            error_root_name: None,
101        }
102    }
103}
104
105impl XmlCodecSettingsBuilder {
106    /// Rejects ill-formed XML and a document root other than the one the
107    /// request schema names. Disabled by default.
108    pub fn enforce_strictness(mut self, value: bool) -> Self {
109        self.validate_document = value;
110        self.validate_root_name = value;
111        self
112    }
113
114    /// Rejects malformed XML and trailing content without changing root-name validation.
115    ///
116    /// Applies to top-level structure, list, map, and scalar reads, including
117    /// skipped content. Checks matching tags, duplicate attribute names, and
118    /// references to the five predefined entities or valid XML 1.0 characters.
119    /// Custom DTD entity references are unsupported.
120    pub fn validate_document(mut self, value: bool) -> Self {
121        self.validate_document = value;
122        self
123    }
124
125    /// Checks the document root against the schema without changing XML recovery.
126    pub fn validate_root_name(mut self, value: bool) -> Self {
127        self.validate_root_name = value;
128        self
129    }
130
131    /// Reads only the children of a wrapped list that are named as its member
132    /// (`member`, or the member's `@xmlName`) and only the `entry` children of
133    /// a wrapped map, skipping the others, as the legacy generated parsers do.
134    ///
135    /// Off by default: every child element is an item or an entry whatever its
136    /// name. Flattened lists and maps are not affected; their elements are
137    /// selected by the structure member's name either way.
138    pub fn strict_collection_element_names(mut self, value: bool) -> Self {
139        self.strict_collection_element_names = value;
140        self
141    }
142
143    /// Overrides the root element of modeled errors while retaining their namespace.
144    pub fn error_root_name(mut self, name: &'static str) -> Self {
145        self.error_root_name = Some(name);
146        self
147    }
148
149    /// Default timestamp format when not specified by `@timestampFormat` trait.
150    pub fn default_timestamp_format(mut self, value: TimestampFormat) -> Self {
151        self.default_timestamp_format = value;
152        self
153    }
154
155    /// Sets the maximum aggregate nesting depth the deserializer will accept
156    /// before returning an error. Defaults to 128.
157    pub fn max_depth(mut self, value: u32) -> Self {
158        self.max_depth = value;
159        self
160    }
161
162    /// Builds the settings.
163    pub fn build(self) -> XmlCodecSettings {
164        XmlCodecSettings {
165            default_timestamp_format: self.default_timestamp_format,
166            max_depth: self.max_depth,
167            validate_document: self.validate_document,
168            validate_root_name: self.validate_root_name,
169            strict_collection_element_names: self.strict_collection_element_names,
170            error_root_name: self.error_root_name,
171        }
172    }
173}
174
175/// XML codec for schema-based serialization and deserialization.
176///
177/// Used by REST XML to serialize request bodies and deserialize response
178/// bodies. The codec carries no state of its own — each `create_serializer`
179/// and `create_deserializer` call returns a fresh instance whose lifetime
180/// brackets one (de)serialization.
181///
182/// # Examples
183///
184/// ```
185/// use aws_smithy_xml::codec::{XmlCodec, XmlCodecSettings};
186/// use aws_smithy_schema::codec::Codec;
187///
188/// let codec = XmlCodec::new(XmlCodecSettings::default());
189/// let _serializer = codec.create_serializer();
190/// let _deserializer = codec.create_deserializer(b"<Root/>");
191/// ```
192#[derive(Debug)]
193pub struct XmlCodec {
194    settings: Arc<XmlCodecSettings>,
195}
196
197impl XmlCodec {
198    /// Creates a new XML codec with the given settings.
199    pub fn new(settings: XmlCodecSettings) -> Self {
200        Self {
201            settings: Arc::new(settings),
202        }
203    }
204
205    /// Creates a new XML codec from a pre-existing shared settings.
206    pub fn from_shared_settings(settings: Arc<XmlCodecSettings>) -> Self {
207        Self { settings }
208    }
209
210    /// Returns the codec settings.
211    pub fn settings(&self) -> &XmlCodecSettings {
212        &self.settings
213    }
214}
215
216impl Default for XmlCodec {
217    fn default() -> Self {
218        Self::new(XmlCodecSettings::default())
219    }
220}
221
222impl Codec for XmlCodec {
223    type Serializer = XmlSerializer;
224    type Deserializer<'a> = XmlDeserializer<'a>;
225
226    fn create_serializer(&self) -> Self::Serializer {
227        XmlSerializer::new(self.settings.clone())
228    }
229
230    fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
231        XmlDeserializer::new(input, self.settings.clone())
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_default_settings() {
241        let settings = XmlCodecSettings::default();
242        assert_eq!(
243            settings.default_timestamp_format(),
244            TimestampFormat::DateTime
245        );
246    }
247
248    #[test]
249    fn test_codec_creation() {
250        let codec = XmlCodec::default();
251        let _serializer = codec.create_serializer();
252        let _deserializer = codec.create_deserializer(b"<Root/>");
253    }
254
255    #[test]
256    fn test_end_to_end_serialize_through_codec() {
257        use aws_smithy_schema::codec::FinishSerializer;
258        use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeSerializer};
259        use aws_smithy_schema::{shape_id, Schema, ShapeType};
260
261        static NAME: Schema<'static> =
262            Schema::new_member(shape_id!("test", "X$name"), ShapeType::String, "name", 0);
263        static X_SCHEMA: Schema<'static> =
264            Schema::new_struct(shape_id!("test", "X"), ShapeType::Structure, &[&NAME])
265                .with_xml_namespace("urn:test", None);
266
267        struct X;
268        impl SerializableStruct for X {
269            fn schema(&self) -> &Schema<'_> {
270                &X_SCHEMA
271            }
272
273            fn serialize_members(&self, ser: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
274                ser.write_string(&NAME, "hello")
275            }
276        }
277
278        let codec = XmlCodec::default();
279        let mut ser = codec.create_serializer();
280        ser.write_struct(&X_SCHEMA, &X).unwrap();
281        let bytes = ser.finish();
282        assert_eq!(
283            String::from_utf8(bytes).unwrap(),
284            "<X xmlns=\"urn:test\"><name>hello</name></X>"
285        );
286    }
287
288    #[test]
289    fn test_end_to_end_deserialize_through_codec() {
290        use aws_smithy_schema::serde::ShapeDeserializer;
291        use aws_smithy_schema::{shape_id, Schema, ShapeType};
292
293        static NAME: Schema<'static> =
294            Schema::new_member(shape_id!("test", "X$name"), ShapeType::String, "name", 0);
295        static AGE: Schema<'static> =
296            Schema::new_member(shape_id!("test", "X$age"), ShapeType::Integer, "age", 1);
297        static X_SCHEMA: Schema<'static> =
298            Schema::new_struct(shape_id!("test", "X"), ShapeType::Structure, &[&NAME, &AGE]);
299
300        let codec = XmlCodec::default();
301        let xml = b"<X><name>Alice</name><age>30</age></X>";
302        let mut deser = codec.create_deserializer(xml);
303
304        let mut name = String::new();
305        let mut age = 0i32;
306        deser
307            .read_struct(&X_SCHEMA, &mut |member, d| {
308                match member.member_name().unwrap() {
309                    "name" => name = d.read_string(member)?,
310                    "age" => age = d.read_integer(member)?,
311                    _ => {}
312                }
313                Ok(())
314            })
315            .unwrap();
316
317        assert_eq!(name, "Alice");
318        assert_eq!(age, 30);
319    }
320}