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;
340pub use de::Deserializer;
341pub use impls::{display_to_xml, from_xml_str, OptionAccumulator};
342pub mod ser;
343pub use ser::Serializer;
344mod any_element;
345pub use any_element::{AnyAttribute, AnyElement};
346
347/// Serialize a type to XML
348pub trait ToXml {
349 /// Serialize this value to XML using the provided serializer
350 fn serialize<W: fmt::Write + ?Sized>(
351 &self,
352 field: Option<Id<'_>>,
353 serializer: &mut Serializer<'_, W>,
354 ) -> Result<(), Error>;
355
356 /// Check if this value should be serialized
357 ///
358 /// Returns `false` for absent optional values, `true` otherwise.
359 fn present(&self) -> bool {
360 true
361 }
362}
363
364impl<T: ToXml + ?Sized> ToXml for &T {
365 fn serialize<W: fmt::Write + ?Sized>(
366 &self,
367 field: Option<Id<'_>>,
368 serializer: &mut Serializer<'_, W>,
369 ) -> Result<(), Error> {
370 (*self).serialize(field, serializer)
371 }
372}
373
374/// Deserialize a type from XML
375pub trait FromXml<'xml>: Sized {
376 /// Check if an element or attribute matches this type
377 fn matches(id: Id<'_>, field: Option<Id<'_>>) -> bool;
378
379 /// Deserialize from XML into an accumulator
380 fn deserialize<'cx>(
381 into: &mut Self::Accumulator,
382 field: &'static str,
383 deserializer: &mut Deserializer<'cx, 'xml>,
384 ) -> Result<(), Error>;
385
386 /// The accumulator type used during deserialization
387 type Accumulator: Accumulate<Self>;
388 /// The kind of XML node this type represents
389 const KIND: Kind;
390}
391
392/// Accumulate values during deserialization
393///
394/// A type implementing `Accumulate<T>` is used to accumulate a value of type `T`.
395pub trait Accumulate<T>: Default {
396 /// Convert the accumulator into the final value, or return an error
397 fn try_done(self, field: &'static str) -> Result<T, Error>;
398}
399
400impl<T> Accumulate<T> for Option<T> {
401 fn try_done(self, field: &'static str) -> Result<T, Error> {
402 self.ok_or(Error::MissingValue(field))
403 }
404}
405
406impl<T> Accumulate<Self> for Vec<T> {
407 fn try_done(self, _: &'static str) -> Result<Self, Error> {
408 Ok(self)
409 }
410}
411
412impl<'a, T> Accumulate<Cow<'a, [T]>> for Vec<T>
413where
414 [T]: ToOwned<Owned = Self>,
415{
416 fn try_done(self, _: &'static str) -> Result<Cow<'a, [T]>, Error> {
417 Ok(Cow::Owned(self))
418 }
419}
420
421impl<T> Accumulate<Self> for Option<T> {
422 fn try_done(self, _: &'static str) -> Result<Self, Error> {
423 Ok(self)
424 }
425}
426
427/// Deserialize a type from an XML string
428///
429/// This is a convenience function that creates a `Deserializer` and calls `deserialize()`.
430pub fn from_str<'xml, T: FromXml<'xml>>(input: &'xml str) -> Result<T, Error> {
431 Deserializer::new(input)?.deserialize()
432}
433
434/// Serialize a value to an XML string
435pub fn to_string(value: &(impl ToXml + ?Sized)) -> Result<String, Error> {
436 let mut output = String::new();
437 to_writer(value, &mut output)?;
438 Ok(output)
439}
440
441/// Serialize a value to an XML writer
442pub fn to_writer(
443 value: &(impl ToXml + ?Sized),
444 output: &mut (impl fmt::Write + ?Sized),
445) -> Result<(), Error> {
446 value.serialize(None, &mut Serializer::new(output))
447}
448
449/// Marker trait for types that can be deserialized with any lifetime
450pub trait FromXmlOwned: for<'xml> FromXml<'xml> {}
451
452impl<T> FromXmlOwned for T where T: for<'xml> FromXml<'xml> {}
453
454/// Errors that can occur during XML serialization and deserialization
455#[derive(Clone, Debug, Eq, Error, PartialEq)]
456pub enum Error {
457 /// Error formatting output
458 #[error("format: {0}")]
459 Format(#[from] fmt::Error),
460 /// Invalid XML entity encountered
461 #[error("invalid entity: {0}")]
462 InvalidEntity(String),
463 /// Error parsing XML
464 #[error("parse: {0}")]
465 Parse(#[from] xmlparser::Error),
466 /// Other error
467 #[error("other: {0}")]
468 Other(String),
469 /// Unexpected end of XML stream
470 #[error("unexpected end of stream")]
471 UnexpectedEndOfStream,
472 /// Unexpected value encountered
473 #[error("unexpected value: '{0}'")]
474 UnexpectedValue(String),
475 /// Unexpected XML tag
476 #[error("unexpected tag: {0}")]
477 UnexpectedTag(String),
478 /// Expected tag but none found
479 #[error("missing tag")]
480 MissingTag,
481 /// Required field has no value
482 #[error("missing value: {0}")]
483 MissingValue(&'static str),
484 /// Unexpected XML token
485 #[error("unexpected token: {0}")]
486 UnexpectedToken(String),
487 /// Unknown namespace prefix
488 #[error("unknown prefix: {0}")]
489 UnknownPrefix(String),
490 /// Unexpected XML node type
491 #[error("unexpected node: {0}")]
492 UnexpectedNode(String),
493 /// Internal state error
494 #[error("unexpected state: {0}")]
495 UnexpectedState(&'static str),
496 /// Expected a scalar value but found an element
497 #[error("expected scalar, found {0}")]
498 ExpectedScalar(String),
499 /// Field value appears more than once
500 #[error("duplicate value for {0}")]
501 DuplicateValue(&'static str),
502}
503
504/// The kind of XML node a type represents
505#[derive(Copy, Clone, Debug, Eq, PartialEq)]
506pub enum Kind {
507 /// A scalar value (text content or attribute)
508 Scalar,
509 /// An XML element
510 Element,
511}
512
513/// Identifier for an XML element or attribute with namespace
514#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
515pub struct Id<'a> {
516 /// The namespace URI
517 pub ns: &'a str,
518 /// The local name
519 pub name: &'a str,
520}