Skip to main content

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 (`&amp;`, 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