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//!   Namespace URIs can be string literals or paths to constants. Prefixes may contain
69//!   dashes and dots: `#[xml(ns(my-ns.v1 = "uri"))]`.
70//!
71//!   ```
72//!   # use instant_xml::{ToXml, to_string};
73//!   #[derive(ToXml)]
74//!   #[xml(ns("http://example.com"))]
75//!   struct Root { }
76//!
77//!   assert_eq!(to_string(&Root {}).unwrap(), r#"<Root xmlns="http://example.com" />"#);
78//!
79//!   #[derive(ToXml)]
80//!   #[xml(ns("http://example.com", xsi = XSI))]
81//!   struct WithPrefix { }
82//!
83//!   assert_eq!(
84//!       to_string(&WithPrefix {}).unwrap(),
85//!       r#"<WithPrefix xmlns="http://example.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />"#
86//!   );
87//!
88//!   const XSI: &'static str = "http://www.w3.org/2001/XMLSchema-instance";
89//!   ```
90//!
91//! - **`transparent`** *(structs only)* - inlines fields without wrapper element
92//!
93//!   ```
94//!   # use instant_xml::{ToXml, to_string};
95//!   #[derive(ToXml)]
96//!   #[xml(transparent)]
97//!   struct Inline {
98//!       foo: Foo,
99//!       bar: Bar,
100//!   }
101//!
102//!   #[derive(ToXml)]
103//!   struct Foo { }
104//!
105//!   #[derive(ToXml)]
106//!   struct Bar { }
107//!
108//!   let inline = Inline { foo: Foo {}, bar: Bar {} };
109//!   assert_eq!(to_string(&inline).unwrap(), "<Foo /><Bar />");
110//!   ```
111//!
112//! - **`scalar`** *(enums only)* - serializes variants as text content.
113//!
114//!   The enum must only have unit variants.
115//!
116//!   ```
117//!   # use instant_xml::{ToXml, to_string};
118//!
119//!   #[derive(ToXml)]
120//!   struct Container {
121//!       status: Status,
122//!   }
123//!
124//!   #[derive(ToXml)]
125//!   #[xml(scalar)]
126//!   enum Status {
127//!       Active,
128//!       Inactive,
129//!   }
130//!
131//!   let c = Container { status: Status::Active };
132//!   assert_eq!(to_string(&c).unwrap(), "<Container><status>Active</status></Container>");
133//!   ```
134//!
135//!   Variants can use `#[xml(rename = "...")]` or string/integer discriminants.
136//!
137//! - **`forward`** *(enums only)* - forwards to inner type's element name.
138//!
139//!   Each variant must contain exactly one unnamed field.
140//!
141//!   ```
142//!   # use instant_xml::{ToXml, to_string};
143//!
144//!   #[derive(ToXml)]
145//!   #[xml(forward)]
146//!   enum Message {
147//!       Request(Request),
148//!       Response(Response),
149//!   }
150//!
151//!   #[derive(ToXml)]
152//!   struct Request { }
153//!
154//!   #[derive(ToXml)]
155//!   struct Response { }
156//!
157//!   let msg = Message::Request(Request {});
158//!   assert_eq!(to_string(&msg).unwrap(), "<Request />");
159//!   ```
160//!
161//! ## Field attributes
162//!
163//! Applied to struct fields using `#[xml(...)]`:
164//!
165//! - **`attribute`** - (de)serializes as XML attribute instead of child element
166//!
167//!   ```
168//!   # use instant_xml::{ToXml, to_string};
169//!   #[derive(ToXml)]
170//!   struct Element {
171//!       #[xml(attribute)]
172//!       id: String,
173//!   }
174//!
175//!   let elem = Element { id: "abc123".to_string() };
176//!   assert_eq!(to_string(&elem).unwrap(), r#"<Element id="abc123" />"#);
177//!   ```
178//!
179//! - **`direct`** - field contains element's direct text content
180//!
181//!   ```
182//!   # use instant_xml::{ToXml, to_string};
183//!   #[derive(ToXml)]
184//!   struct Paragraph {
185//!       #[xml(attribute)]
186//!       lang: String,
187//!       #[xml(direct)]
188//!       text: String,
189//!   }
190//!
191//!   let p = Paragraph { lang: "en".to_string(), text: "Hello".to_string() };
192//!   assert_eq!(to_string(&p).unwrap(), r#"<Paragraph lang="en">Hello</Paragraph>"#);
193//!   ```
194//!
195//! - **`rename = "name"`** - renames the field's element or attribute name
196//!
197//! - **`ns("uri")`** - sets namespace for this specific field. Like the container-level
198//!   attribute, this supports both string literals and constant paths.
199//!
200//! - **`serialize_with = "path"`** - custom serialization function with signature:
201//!
202//!   ```
203//!   # use instant_xml::{Error, Serializer, ToXml, to_string};
204//!   # use std::fmt;
205//!   #[derive(ToXml)]
206//!   struct Config {
207//!       #[xml(serialize_with = "serialize_custom")]
208//!       count: u32,
209//!   }
210//!
211//!   fn serialize_custom<W: fmt::Write + ?Sized>(
212//!       value: &u32,
213//!       serializer: &mut Serializer<'_, W>,
214//!   ) -> Result<(), Error> {
215//!       serializer.write_str(&format!("value: {}", value))?;
216//!       Ok(())
217//!   }
218//!
219//!   let config = Config { count: 42 };
220//!   assert_eq!(to_string(&config).unwrap(), "<Config>value: 42</Config>");
221//!   ```
222//!
223//! - **`deserialize_with = "path"`** - custom deserialization function with signature:
224//!
225//!   ```
226//!   # use instant_xml::{Deserializer, Error, FromXml, from_str};
227//!   #[derive(FromXml, PartialEq, Debug)]
228//!   struct Config {
229//!       #[xml(deserialize_with = "deserialize_bool")]
230//!       enabled: bool,
231//!   }
232//!
233//!   fn deserialize_bool<'xml>(
234//!       accumulator: &mut <bool as FromXml<'xml>>::Accumulator,
235//!       field: &'static str,
236//!       deserializer: &mut Deserializer<'_, 'xml>,
237//!   ) -> Result<(), Error> {
238//!       if accumulator.is_some() {
239//!           return Err(Error::DuplicateValue(field));
240//!       }
241//!
242//!       let Some(s) = deserializer.take_str()? else {
243//!           return Ok(());
244//!       };
245//!
246//!       *accumulator = Some(match s.as_ref() {
247//!           "yes" => true,
248//!           "no" => false,
249//!           other => return Err(Error::UnexpectedValue(
250//!               format!("expected 'yes' or 'no', got '{}'", other)
251//!           )),
252//!       });
253//!
254//!       deserializer.ignore()?;
255//!       Ok(())
256//!   }
257//!
258//!   let xml = "<Config><enabled>yes</enabled></Config>";
259//!   let config = from_str::<Config>(xml).unwrap();
260//!   assert_eq!(config.enabled, true);
261//!   ```
262//!
263//! - **`borrow`** - Borrows from input during deserialization. Automatically applies to
264//!   top-level `&str` and `&[u8]` fields. Useful for `Cow<str>` and similar types.
265//!
266//!   ```
267//!   # use instant_xml::{FromXml, from_str};
268//!   # use std::borrow::Cow;
269//!   #[derive(FromXml, PartialEq, Debug)]
270//!   struct Borrowed<'a> {
271//!       #[xml(borrow)]
272//!       text: Cow<'a, str>,
273//!   }
274//!
275//!   let xml = "<Borrowed><text>Hello</text></Borrowed>";
276//!   let parsed = from_str::<Borrowed>(xml).unwrap();
277//!   assert_eq!(parsed.text, "Hello");
278//!   ```
279
280use std::{borrow::Cow, fmt};
281
282use thiserror::Error;
283
284pub use macros::{FromXml, ToXml};
285
286#[doc(hidden)]
287pub mod de;
288mod impls;
289use de::Context;
290pub use de::Deserializer;
291pub use impls::{display_to_xml, from_xml_str, OptionAccumulator};
292#[doc(hidden)]
293pub mod ser;
294pub use ser::Serializer;
295
296pub trait ToXml {
297    fn serialize<W: fmt::Write + ?Sized>(
298        &self,
299        field: Option<Id<'_>>,
300        serializer: &mut Serializer<W>,
301    ) -> Result<(), Error>;
302
303    fn present(&self) -> bool {
304        true
305    }
306}
307
308impl<T: ToXml + ?Sized> ToXml for &T {
309    fn serialize<W: fmt::Write + ?Sized>(
310        &self,
311        field: Option<Id<'_>>,
312        serializer: &mut Serializer<W>,
313    ) -> Result<(), Error> {
314        (*self).serialize(field, serializer)
315    }
316}
317
318pub trait FromXml<'xml>: Sized {
319    fn matches(id: Id<'_>, field: Option<Id<'_>>) -> bool;
320
321    fn deserialize<'cx>(
322        into: &mut Self::Accumulator,
323        field: &'static str,
324        deserializer: &mut Deserializer<'cx, 'xml>,
325    ) -> Result<(), Error>;
326
327    type Accumulator: Accumulate<Self>;
328    const KIND: Kind;
329}
330
331/// A type implementing `Accumulate<T>` is used to accumulate a value of type `T`.
332pub trait Accumulate<T>: Default {
333    fn try_done(self, field: &'static str) -> Result<T, Error>;
334}
335
336impl<T> Accumulate<T> for Option<T> {
337    fn try_done(self, field: &'static str) -> Result<T, Error> {
338        self.ok_or(Error::MissingValue(field))
339    }
340}
341
342impl<T> Accumulate<Vec<T>> for Vec<T> {
343    fn try_done(self, _: &'static str) -> Result<Vec<T>, Error> {
344        Ok(self)
345    }
346}
347
348impl<'a, T> Accumulate<Cow<'a, [T]>> for Vec<T>
349where
350    [T]: ToOwned<Owned = Vec<T>>,
351{
352    fn try_done(self, _: &'static str) -> Result<Cow<'a, [T]>, Error> {
353        Ok(Cow::Owned(self))
354    }
355}
356
357impl<T> Accumulate<Option<T>> for Option<T> {
358    fn try_done(self, _: &'static str) -> Result<Option<T>, Error> {
359        Ok(self)
360    }
361}
362
363pub fn from_str<'xml, T: FromXml<'xml>>(input: &'xml str) -> Result<T, Error> {
364    let (mut context, root) = Context::new(input)?;
365    let id = context.element_id(&root)?;
366
367    if !T::matches(id, None) {
368        return Err(Error::UnexpectedValue(match id.ns.is_empty() {
369            true => format!("unexpected root element {:?}", id.name),
370            false => format!(
371                "unexpected root element {:?} in namespace {:?}",
372                id.name, id.ns
373            ),
374        }));
375    }
376
377    let mut value = T::Accumulator::default();
378    T::deserialize(
379        &mut value,
380        "<root element>",
381        &mut Deserializer::new(root, &mut context),
382    )?;
383    value.try_done("<root element>")
384}
385
386pub fn to_string(value: &(impl ToXml + ?Sized)) -> Result<String, Error> {
387    let mut output = String::new();
388    to_writer(value, &mut output)?;
389    Ok(output)
390}
391
392pub fn to_writer(
393    value: &(impl ToXml + ?Sized),
394    output: &mut (impl fmt::Write + ?Sized),
395) -> Result<(), Error> {
396    value.serialize(None, &mut Serializer::new(output))
397}
398
399pub trait FromXmlOwned: for<'xml> FromXml<'xml> {}
400
401impl<T> FromXmlOwned for T where T: for<'xml> FromXml<'xml> {}
402
403#[derive(Clone, Debug, Eq, Error, PartialEq)]
404pub enum Error {
405    #[error("format: {0}")]
406    Format(#[from] fmt::Error),
407    #[error("invalid entity: {0}")]
408    InvalidEntity(String),
409    #[error("parse: {0}")]
410    Parse(#[from] xmlparser::Error),
411    #[error("other: {0}")]
412    Other(std::string::String),
413    #[error("unexpected end of stream")]
414    UnexpectedEndOfStream,
415    #[error("unexpected value: '{0}'")]
416    UnexpectedValue(String),
417    #[error("unexpected tag: {0}")]
418    UnexpectedTag(String),
419    #[error("missing tag")]
420    MissingTag,
421    #[error("missing value: {0}")]
422    MissingValue(&'static str),
423    #[error("unexpected token: {0}")]
424    UnexpectedToken(String),
425    #[error("unknown prefix: {0}")]
426    UnknownPrefix(String),
427    #[error("unexpected node: {0}")]
428    UnexpectedNode(String),
429    #[error("unexpected state: {0}")]
430    UnexpectedState(&'static str),
431    #[error("expected scalar, found {0}")]
432    ExpectedScalar(String),
433    #[error("duplicate value for {0}")]
434    DuplicateValue(&'static str),
435}
436
437#[derive(Copy, Clone, Debug, Eq, PartialEq)]
438pub enum Kind {
439    Scalar,
440    Element,
441}
442
443#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
444pub struct Id<'a> {
445    pub ns: &'a str,
446    pub name: &'a str,
447}