Skip to main content

instant_xml/
lib.rs

1//! A serde-like library for rigorous XML (de)serialization.
2//!
3//! instant-xml provides traits and derive macros for mapping XML to Rust types,
4//! with full support for XML namespaces and zero-copy deserialization.
5//!
6//! # Quick Start
7//!
8//! ```
9//! # use instant_xml::{FromXml, ToXml, from_str, to_string};
10//! #[derive(Debug, PartialEq, FromXml, ToXml)]
11//! struct Person {
12//!     name: String,
13//!     #[xml(attribute)]
14//!     age: u32,
15//! }
16//!
17//! let person = Person {
18//!     name: "Alice".to_string(),
19//!     age: 30,
20//! };
21//!
22//! let xml = to_string(&person).unwrap();
23//! assert_eq!(xml, r#"<Person age="30"><name>Alice</name></Person>"#);
24//!
25//! let deserialized: Person = from_str(&xml).unwrap();
26//! assert_eq!(person, deserialized);
27//! ```
28//!
29//! # `#[xml(...)]` attribute reference
30//!
31//! The `#[xml(...)]` attribute configures serialization and deserialization behavior
32//! for the [`ToXml`] and [`FromXml`] derive macros.
33//!
34//! ## Container attributes
35//!
36//! Applied to structs and enums using `#[xml(...)]`:
37//!
38//! - **`rename = "name"`** - renames the root element
39//!
40//!   ```
41//!   # use instant_xml::{ToXml, to_string};
42//!   #[derive(ToXml)]
43//!   #[xml(rename = "custom-name")]
44//!   struct MyStruct { }
45//!
46//!   assert_eq!(to_string(&MyStruct {}).unwrap(), "<custom-name />");
47//!   ```
48//!
49//! - **`rename_all = "case"`** - transforms all field/variant names.
50//!
51//!   Supported cases: `"lowercase"`, `"UPPERCASE"`, `"PascalCase"`, `"camelCase"`,
52//!   `"snake_case"`, `"SCREAMING_SNAKE_CASE"`, `"kebab-case"`, `"SCREAMING-KEBAB-CASE"`.
53//!
54//!   ```
55//!   # use instant_xml::{ToXml, to_string};
56//!   #[derive(ToXml)]
57//!   #[xml(rename_all = "camelCase")]
58//!   struct MyStruct {
59//!       field_one: String,
60//!   }
61//!
62//!   let s = MyStruct { field_one: "value".to_string() };
63//!   assert_eq!(to_string(&s).unwrap(), "<MyStruct><fieldOne>value</fieldOne></MyStruct>");
64//!   ```
65//!
66//! - **`ns("uri")` or `ns("uri", prefix = "namespace")`** - configures XML namespaces
67//!
68//!   The first positional argument sets the namespace for the element. If the parent's namespace
69//!   differs and no ancestors set a prefix for this namespace, a `xmlns="uri"` declaration is
70//!   emitted on the element. If a prefix is declared for the namespace, it is used. Fields
71//!   without their own `ns(...)` inherit the type's namespace.
72//!
73//!   Additional `prefix = "uri"` entries declare prefix mappings that are emitted as
74//!   `xmlns:prefix="uri"` on the element. These prefixes can then be referenced by
75//!   fields and child elements. Namespace URIs can be string literals or paths to constants.
76//!   Prefix names may contain dashes and dots: `#[xml(ns(my-ns.v1 = "uri"))]`.
77//!
78//!   ```
79//!   # use instant_xml::{ToXml, to_string};
80//!   #[derive(ToXml)]
81//!   #[xml(ns("http://example.com"))]
82//!   struct Root { }
83//!
84//!   assert_eq!(to_string(&Root {}).unwrap(), r#"<Root xmlns="http://example.com" />"#);
85//!
86//!   #[derive(ToXml)]
87//!   #[xml(ns("http://example.com", xsi = XSI))]
88//!   struct WithPrefix { }
89//!
90//!   assert_eq!(
91//!       to_string(&WithPrefix {}).unwrap(),
92//!       r#"<WithPrefix xmlns="http://example.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />"#
93//!   );
94//!
95//!   const XSI: &'static str = "http://www.w3.org/2001/XMLSchema-instance";
96//!   ```
97//!
98//!   When a child struct has a different default namespace than its parent, a new `xmlns="..."` is
99//!   emitted on the child element. Prefix declarations from the parent are inherited and not
100//!   redeclared.
101//!
102//! - **`transparent`** *(structs only)* - inlines fields without wrapper element
103//!
104//!   ```
105//!   # use instant_xml::{ToXml, to_string};
106//!   #[derive(ToXml)]
107//!   #[xml(transparent)]
108//!   struct Inline {
109//!       foo: Foo,
110//!       bar: Bar,
111//!   }
112//!
113//!   #[derive(ToXml)]
114//!   struct Foo { }
115//!
116//!   #[derive(ToXml)]
117//!   struct Bar { }
118//!
119//!   let inline = Inline { foo: Foo {}, bar: Bar {} };
120//!   assert_eq!(to_string(&inline).unwrap(), "<Foo /><Bar />");
121//!   ```
122//!
123//! - **`scalar`** *(enums only)* - serializes variants as text content.
124//!
125//!   The enum must only have unit variants.
126//!
127//!   ```
128//!   # use instant_xml::{ToXml, to_string};
129//!
130//!   #[derive(ToXml)]
131//!   struct Container {
132//!       status: Status,
133//!   }
134//!
135//!   #[derive(ToXml)]
136//!   #[xml(scalar)]
137//!   enum Status {
138//!       Active,
139//!       Inactive,
140//!   }
141//!
142//!   let c = Container { status: Status::Active };
143//!   assert_eq!(to_string(&c).unwrap(), "<Container><status>Active</status></Container>");
144//!   ```
145//!
146//!   Variants can use `#[xml(rename = "...")]` or string/integer discriminants.
147//!
148//! - **`forward`** *(enums only)* - forwards to inner type's element name.
149//!
150//!   Each variant must contain exactly one unnamed field.
151//!
152//!   ```
153//!   # use instant_xml::{ToXml, to_string};
154//!
155//!   #[derive(ToXml)]
156//!   #[xml(forward)]
157//!   enum Message {
158//!       Request(Request),
159//!       Response(Response),
160//!   }
161//!
162//!   #[derive(ToXml)]
163//!   struct Request { }
164//!
165//!   #[derive(ToXml)]
166//!   struct Response { }
167//!
168//!   let msg = Message::Request(Request {});
169//!   assert_eq!(to_string(&msg).unwrap(), "<Request />");
170//!   ```
171//!
172//! -**`force_prefix`** *(structs only)* - Always serialize a namespace prefix if one is set for this element's namespace.
173//! Does not affect deserialization.
174//!
175//! ## Field attributes
176//!
177//! Applied to struct fields using `#[xml(...)]`:
178//!
179//! - **`attribute`** - (de)serializes as XML attribute instead of child element
180//!
181//!   ```
182//!   # use instant_xml::{ToXml, to_string};
183//!   #[derive(ToXml)]
184//!   struct Element {
185//!       #[xml(attribute)]
186//!       id: String,
187//!   }
188//!
189//!   let elem = Element { id: "abc123".to_string() };
190//!   assert_eq!(to_string(&elem).unwrap(), r#"<Element id="abc123" />"#);
191//!   ```
192//!
193//! - **`direct`** - field contains element's direct text content
194//!
195//!   ```
196//!   # use instant_xml::{ToXml, to_string};
197//!   #[derive(ToXml)]
198//!   struct Paragraph {
199//!       #[xml(attribute)]
200//!       lang: String,
201//!       #[xml(direct)]
202//!       text: String,
203//!   }
204//!
205//!   let p = Paragraph { lang: "en".to_string(), text: "Hello".to_string() };
206//!   assert_eq!(to_string(&p).unwrap(), r#"<Paragraph lang="en">Hello</Paragraph>"#);
207//!   ```
208//!
209//! - **`rename = "name"`** - renames the field's element or attribute name
210//!
211//! - **`ns("uri")`** - sets namespace for this specific field
212//!
213//!   Like the container-level attribute, this supports both string literals and constant
214//!   paths. How the namespace is serialized depends on whether it matches a prefix
215//!   declared on the container:
216//!
217//!   - If the URI matches a declared prefix, the field is serialized with that prefix
218//!     (`<bar:field>`).
219//!   - If the URI does not match any declared prefix, a `xmlns="uri"` is emitted
220//!     directly on the field's element.
221//!   - Fields without `ns(...)` inherit the container's namespace.
222//!
223//!   For `attribute` fields, the namespace must reference a URI that has a declared
224//!   prefix (XML attributes cannot use unprefixed default namespaces).
225//!
226//!   ```
227//!   # use instant_xml::{ToXml, to_string};
228//!   const BAZ: &str = "http://baz.example.com";
229//!
230//!   #[derive(ToXml)]
231//!   #[xml(ns("http://example.com", bar = BAZ))]
232//!   struct Example {
233//!       plain: bool,                // inherits default ns "http://example.com"
234//!       #[xml(ns(BAZ))]
235//!       prefixed: String,           // matches prefix "bar", serialized as <bar:prefixed>
236//!       #[xml(ns("http://other"))]
237//!       direct: i32,                // no matching prefix, emits xmlns="http://other"
238//!   }
239//!
240//!   assert_eq!(
241//!       to_string(&Example { plain: true, prefixed: "val".into(), direct: 1 }).unwrap(),
242//!       concat!(
243//!           r#"<Example xmlns="http://example.com" xmlns:bar="http://baz.example.com">"#,
244//!           "<plain>true</plain>",
245//!           "<bar:prefixed>val</bar:prefixed>",
246//!           r#"<direct xmlns="http://other">1</direct>"#,
247//!           "</Example>",
248//!       ),
249//!   );
250//!   ```
251//!
252//! - **`serialize_with = "path"`** - custom serialization function with signature:
253//!
254//!   ```
255//!   # use instant_xml::{Error, Serializer, ToXml, to_string};
256//!   # use std::fmt;
257//!   #[derive(ToXml)]
258//!   struct Config {
259//!       #[xml(serialize_with = "serialize_custom")]
260//!       count: u32,
261//!   }
262//!
263//!   fn serialize_custom<W: fmt::Write + ?Sized>(
264//!       value: &u32,
265//!       serializer: &mut Serializer<'_, W>,
266//!   ) -> Result<(), Error> {
267//!       serializer.write_str(&format!("value: {}", value))?;
268//!       Ok(())
269//!   }
270//!
271//!   let config = Config { count: 42 };
272//!   assert_eq!(to_string(&config).unwrap(), "<Config>value: 42</Config>");
273//!   ```
274//!
275//! - **`deserialize_with = "path"`** - custom deserialization function with signature:
276//!
277//!   ```
278//!   # use instant_xml::{Deserializer, Error, FromXml, from_str};
279//!   #[derive(FromXml, PartialEq, Debug)]
280//!   struct Config {
281//!       #[xml(deserialize_with = "deserialize_bool")]
282//!       enabled: bool,
283//!   }
284//!
285//!   fn deserialize_bool<'xml>(
286//!       accumulator: &mut <bool as FromXml<'xml>>::Accumulator,
287//!       field: &'static str,
288//!       deserializer: &mut Deserializer<'_, 'xml>,
289//!   ) -> Result<(), Error> {
290//!       if accumulator.is_some() {
291//!           return Err(Error::DuplicateValue(field));
292//!       }
293//!
294//!       let Some(s) = deserializer.take_str()? else {
295//!           return Ok(());
296//!       };
297//!
298//!       *accumulator = Some(match s.as_ref() {
299//!           "yes" => true,
300//!           "no" => false,
301//!           other => return Err(Error::UnexpectedValue(
302//!               format!("expected 'yes' or 'no', got '{}'", other)
303//!           )),
304//!       });
305//!
306//!       deserializer.ignore()?;
307//!       Ok(())
308//!   }
309//!
310//!   let xml = "<Config><enabled>yes</enabled></Config>";
311//!   let config = from_str::<Config>(xml).unwrap();
312//!   assert_eq!(config.enabled, true);
313//!   ```
314//!
315//! - **`borrow`** - Borrows from input during deserialization. Automatically applies to
316//!   top-level `&str` and `&[u8]` fields. Useful for `Cow<str>` and similar types.
317//!
318//!   ```
319//!   # use instant_xml::{FromXml, from_str};
320//!   # use std::borrow::Cow;
321//!   #[derive(FromXml, PartialEq, Debug)]
322//!   struct Borrowed<'a> {
323//!       #[xml(borrow)]
324//!       text: Cow<'a, str>,
325//!   }
326//!
327//!   let xml = "<Borrowed><text>Hello</text></Borrowed>";
328//!   let parsed = from_str::<Borrowed>(xml).unwrap();
329//!   assert_eq!(parsed.text, "Hello");
330//!   ```
331
332use std::{borrow::Cow, fmt};
333
334use thiserror::Error;
335
336pub use macros::{FromXml, ToXml};
337
338pub mod de;
339mod impls;
340use de::Context;
341pub use de::Deserializer;
342pub use impls::{display_to_xml, from_xml_str, OptionAccumulator};
343pub mod ser;
344pub use ser::Serializer;
345mod any_element;
346pub use any_element::{AnyAttribute, AnyElement};
347
348/// Serialize a type to XML
349pub trait ToXml {
350    /// Serialize this value to XML using the provided serializer
351    fn serialize<W: fmt::Write + ?Sized>(
352        &self,
353        field: Option<Id<'_>>,
354        serializer: &mut Serializer<'_, W>,
355    ) -> Result<(), Error>;
356
357    /// Check if this value should be serialized
358    ///
359    /// Returns `false` for absent optional values, `true` otherwise.
360    fn present(&self) -> bool {
361        true
362    }
363}
364
365impl<T: ToXml + ?Sized> ToXml for &T {
366    fn serialize<W: fmt::Write + ?Sized>(
367        &self,
368        field: Option<Id<'_>>,
369        serializer: &mut Serializer<'_, W>,
370    ) -> Result<(), Error> {
371        (*self).serialize(field, serializer)
372    }
373}
374
375/// Deserialize a type from XML
376pub trait FromXml<'xml>: Sized {
377    /// Check if an element or attribute matches this type
378    fn matches(id: Id<'_>, field: Option<Id<'_>>) -> bool;
379
380    /// Deserialize from XML into an accumulator
381    fn deserialize<'cx>(
382        into: &mut Self::Accumulator,
383        field: &'static str,
384        deserializer: &mut Deserializer<'cx, 'xml>,
385    ) -> Result<(), Error>;
386
387    /// The accumulator type used during deserialization
388    type Accumulator: Accumulate<Self>;
389    /// The kind of XML node this type represents
390    const KIND: Kind;
391}
392
393/// Accumulate values during deserialization
394///
395/// A type implementing `Accumulate<T>` is used to accumulate a value of type `T`.
396pub trait Accumulate<T>: Default {
397    /// Convert the accumulator into the final value, or return an error
398    fn try_done(self, field: &'static str) -> Result<T, Error>;
399}
400
401impl<T> Accumulate<T> for Option<T> {
402    fn try_done(self, field: &'static str) -> Result<T, Error> {
403        self.ok_or(Error::MissingValue(field))
404    }
405}
406
407impl<T> Accumulate<Self> for Vec<T> {
408    fn try_done(self, _: &'static str) -> Result<Self, Error> {
409        Ok(self)
410    }
411}
412
413impl<'a, T> Accumulate<Cow<'a, [T]>> for Vec<T>
414where
415    [T]: ToOwned<Owned = Self>,
416{
417    fn try_done(self, _: &'static str) -> Result<Cow<'a, [T]>, Error> {
418        Ok(Cow::Owned(self))
419    }
420}
421
422impl<T> Accumulate<Self> for Option<T> {
423    fn try_done(self, _: &'static str) -> Result<Self, Error> {
424        Ok(self)
425    }
426}
427
428/// Deserialize a type from an XML string
429pub fn from_str<'xml, T: FromXml<'xml>>(input: &'xml str) -> Result<T, Error> {
430    let (mut context, root) = Context::new(input)?;
431    let id = context.element_id(&root)?;
432
433    if !T::matches(id, None) {
434        return Err(Error::UnexpectedValue(match id.ns.is_empty() {
435            true => format!("unexpected root element {:?}", id.name),
436            false => format!(
437                "unexpected root element {:?} in namespace {:?}",
438                id.name, id.ns
439            ),
440        }));
441    }
442
443    let mut value = T::Accumulator::default();
444    T::deserialize(
445        &mut value,
446        "<root element>",
447        &mut Deserializer::new(root, &mut context),
448    )?;
449    value.try_done("<root element>")
450}
451
452/// Serialize a value to an XML string
453pub fn to_string(value: &(impl ToXml + ?Sized)) -> Result<String, Error> {
454    let mut output = String::new();
455    to_writer(value, &mut output)?;
456    Ok(output)
457}
458
459/// Serialize a value to an XML writer
460pub fn to_writer(
461    value: &(impl ToXml + ?Sized),
462    output: &mut (impl fmt::Write + ?Sized),
463) -> Result<(), Error> {
464    value.serialize(None, &mut Serializer::new(output))
465}
466
467/// Marker trait for types that can be deserialized with any lifetime
468pub trait FromXmlOwned: for<'xml> FromXml<'xml> {}
469
470impl<T> FromXmlOwned for T where T: for<'xml> FromXml<'xml> {}
471
472/// Errors that can occur during XML serialization and deserialization
473#[derive(Clone, Debug, Eq, Error, PartialEq)]
474pub enum Error {
475    /// Error formatting output
476    #[error("format: {0}")]
477    Format(#[from] fmt::Error),
478    /// Invalid XML entity encountered
479    #[error("invalid entity: {0}")]
480    InvalidEntity(String),
481    /// Error parsing XML
482    #[error("parse: {0}")]
483    Parse(#[from] xmlparser::Error),
484    /// Other error
485    #[error("other: {0}")]
486    Other(String),
487    /// Unexpected end of XML stream
488    #[error("unexpected end of stream")]
489    UnexpectedEndOfStream,
490    /// Unexpected value encountered
491    #[error("unexpected value: '{0}'")]
492    UnexpectedValue(String),
493    /// Unexpected XML tag
494    #[error("unexpected tag: {0}")]
495    UnexpectedTag(String),
496    /// Expected tag but none found
497    #[error("missing tag")]
498    MissingTag,
499    /// Required field has no value
500    #[error("missing value: {0}")]
501    MissingValue(&'static str),
502    /// Unexpected XML token
503    #[error("unexpected token: {0}")]
504    UnexpectedToken(String),
505    /// Unknown namespace prefix
506    #[error("unknown prefix: {0}")]
507    UnknownPrefix(String),
508    /// Unexpected XML node type
509    #[error("unexpected node: {0}")]
510    UnexpectedNode(String),
511    /// Internal state error
512    #[error("unexpected state: {0}")]
513    UnexpectedState(&'static str),
514    /// Expected a scalar value but found an element
515    #[error("expected scalar, found {0}")]
516    ExpectedScalar(String),
517    /// Field value appears more than once
518    #[error("duplicate value for {0}")]
519    DuplicateValue(&'static str),
520}
521
522/// The kind of XML node a type represents
523#[derive(Copy, Clone, Debug, Eq, PartialEq)]
524pub enum Kind {
525    /// A scalar value (text content or attribute)
526    Scalar,
527    /// An XML element
528    Element,
529}
530
531/// Identifier for an XML element or attribute with namespace
532#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
533pub struct Id<'a> {
534    /// The namespace URI
535    pub ns: &'a str,
536    /// The local name
537    pub name: &'a str,
538}