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}