sup_xml/de.rs
1//! Serde-driven XML deserialization.
2//!
3//! Map XML directly into typed Rust values:
4//!
5//! ```
6//! use serde::Deserialize;
7//!
8//! #[derive(Deserialize, Debug, PartialEq)]
9//! struct Page {
10//! #[serde(rename = "@id")]
11//! id: u32,
12//! title: String,
13//! }
14//!
15//! let xml = r#"<page id="7"><title>hello</title></page>"#;
16//! let page: Page = sup_xml::de::from_str(xml).unwrap();
17//! assert_eq!(page, Page { id: 7, title: "hello".into() });
18//! ```
19//!
20//! # XML → serde conventions
21//!
22//! | XML construct | Serde concept | Field name |
23//! |------------------------------|---------------------------|--------------------|
24//! | Element attribute | struct field | `@name` |
25//! | Element text-only content | scalar / string field | (the field itself) |
26//! | Element with children | struct | element name |
27//! | Repeated same-name children | `Vec<T>` | element name |
28//! | Optional element | `Option<T>` | element name |
29//! | Mixed text + child elements | struct field | `$text` |
30//! | Heterogeneous element body | enum sequence | `$value` |
31//!
32//! `$text` collects all text/CDATA between the start and end tag — atomic
33//! content only (primitives, strings, unit enums, space-delimited lists).
34//! `$value` collects child *elements* — each one becomes a sequence item,
35//! and for enum types the element tag picks the variant. If both are
36//! declared on the same struct, text routes to `$text` and remaining child
37//! elements route to `$value`.
38//!
39//! # Borrowed deserialization
40//!
41//! Deserializing into `&'de str` borrows directly from the source. This
42//! works only when **all** of the following hold for the field's content:
43//!
44//! 1. The input is UTF-8 (always true for [`from_str`]).
45//! 2. The text contains no entity references (`&`, etc.).
46//! 3. The text contains no character references (`&#xNN;`).
47//! 4. The text comes from a *single* `Text` or `CData` event — adjacent
48//! text and CDATA segments get merged into an owned `String` and lose
49//! borrow eligibility.
50//!
51//! Use `String` (or `Cow<'_, str>`) to handle either case.
52//!
53//! # Options
54//!
55//! Pass [`DeOptions`] to [`from_str_opts`] / [`from_bytes_opts`] to tune:
56//! the underlying `ParseOptions`, the magic field names, the attribute
57//! prefix, unknown-field handling, and `xsi:nil` behaviour.
58
59use std::fmt;
60
61use serde::de::{self, Deserialize, Error as _};
62use sup_xml_core::ParseOptions;
63
64mod deserializer;
65
66pub use deserializer::XmlDeserializer;
67
68// ── public entry points ──────────────────────────────────────────────────────
69
70/// Deserialize a Rust value from an XML string. Borrows from the input
71/// where possible (see the [borrowed deserialization](crate::de#borrowed-deserialization)
72/// section for the precise constraints).
73pub fn from_str<'de, T: Deserialize<'de>>(s: &'de str) -> Result<T, DeError> {
74 from_str_opts(s, DeOptions::default())
75}
76
77/// Deserialize from an XML byte slice. The bytes must be valid UTF-8.
78pub fn from_bytes<'de, T: Deserialize<'de>>(b: &'de [u8]) -> Result<T, DeError> {
79 from_bytes_opts(b, DeOptions::default())
80}
81
82/// Like [`from_str`], with caller-supplied [`DeOptions`].
83pub fn from_str_opts<'de, T: Deserialize<'de>>(s: &'de str, opts: DeOptions) -> Result<T, DeError> {
84 let mut de = XmlDeserializer::from_str_opts(s, opts);
85 let t = T::deserialize(&mut de)?;
86 Ok(t)
87}
88
89/// Like [`from_bytes`], with caller-supplied [`DeOptions`].
90pub fn from_bytes_opts<'de, T: Deserialize<'de>>(b: &'de [u8], opts: DeOptions) -> Result<T, DeError> {
91 let s = simdutf8::compat::from_utf8(b).map_err(|e| DeError::custom(format!("invalid UTF-8: {e}")))?;
92 from_str_opts(s, opts)
93}
94
95// ── options ──────────────────────────────────────────────────────────────────
96
97/// Tunables for the deserializer. Defaults match quick-xml's conventions
98/// for drop-in compatibility.
99#[derive(Debug, Clone)]
100pub struct DeOptions {
101 /// Forwarded to the underlying [`XmlReader`](crate::XmlReader).
102 pub parse: ParseOptions,
103 /// Field name that collects element text content. Default `"$text"`.
104 pub text_field_name: &'static str,
105 /// Field name that collects heterogeneous child elements as a sequence.
106 /// Default `"$value"`.
107 pub value_field_name: &'static str,
108 /// Prefix that marks a field as mapping to an XML attribute.
109 /// Default `'@'`.
110 pub attribute_prefix: char,
111 /// If false, unknown attributes and unknown child elements produce an
112 /// error instead of being skipped. Default `true`.
113 pub allow_unknown_fields: bool,
114 /// If true, an element carrying `xsi:nil="true"` deserializes to
115 /// serde `None` and its content is skipped. Default `true`.
116 pub honor_xsi_nil: bool,
117}
118
119impl Default for DeOptions {
120 fn default() -> Self {
121 Self {
122 parse: ParseOptions::default(),
123 text_field_name: "$text",
124 value_field_name: "$value",
125 attribute_prefix: '@',
126 allow_unknown_fields: true,
127 honor_xsi_nil: true,
128 }
129 }
130}
131
132// ── error type ───────────────────────────────────────────────────────────────
133
134/// Error returned by the deserializer.
135#[derive(Debug, Clone)]
136pub struct DeError {
137 /// Human-readable description of the failure. Includes both serde-side
138 /// errors (type mismatch, missing field, bad number format) and
139 /// parser-side errors (malformed XML, unterminated tag) bubbled up
140 /// from the underlying [`XmlReader`](crate::XmlReader).
141 pub message: String,
142}
143
144impl DeError {
145 pub(crate) fn msg(s: impl Into<String>) -> Self {
146 Self { message: s.into() }
147 }
148}
149
150impl fmt::Display for DeError {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 f.write_str(&self.message)
153 }
154}
155
156impl std::error::Error for DeError {}
157
158impl de::Error for DeError {
159 fn custom<T: fmt::Display>(msg: T) -> Self {
160 Self { message: msg.to_string() }
161 }
162}
163
164impl From<sup_xml_core::XmlError> for DeError {
165 fn from(e: sup_xml_core::XmlError) -> Self {
166 Self { message: e.to_string() }
167 }
168}
169