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}