// Copyright (c) 2024 Jonas Schäfer // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. //! # Convert data to and from XML text //! //! This module provides traits and types related to conversion of XML text //! data to and from Rust types, as well as the [`AsXmlText`], //! [`AsOptionalXmlText`][`crate::AsOptionalXmlText`] and [`FromXmlText`] //! implementations for foreign and standard-library types. //! //! ## Support for types from third-party crates //! //! Beyond the standard library types, the following additional types are //! supported: //! //! | Feature gate | Types | //! | --- | --- | //! | `jid` | `jid::Jid`, `jid::BareJid`, `jid::FullJid` | //! | `serde_json` | `serde_json::Value` | //! | `uuid` | `uuid::Uuid` | //! //! ### Adding support for more types //! //! Due to the orphan rule, it is not possible for applications to implement //! [`AsXmlText`], [`AsOptionalXmlText`][`crate::AsOptionalXmlText`] or //! [`FromXmlText`] on types which originate from third-party crates. Because //! of that, we are **extremely liberal** at accepting merge requests for //! implementations of these traits for types from third-party crates. //! //! The only requirement is that the implementation is gated behind a feature //! flag which is disabled-by-default. //! //! ### Workaround for unsupported types //! //! If making a merge request against `xso` and waiting for a release is not //! an option, you can use newtype wrappers in almost all cases, for example: //! #![cfg_attr( not(all(feature = "std", feature = "macros")), doc = "Because the std or macros feature was not enabled at doc build time, the example cannot be tested.\n\n```ignore\n" )] #![cfg_attr(all(feature = "std", feature = "macros"), doc = "\n```\n")] //! # use xso::{AsXml, FromXml, AsXmlText, FromXmlText, error::Error}; //! # use std::borrow::Cow; //! use std::process::ExitCode; //! //! struct MyExitCode(ExitCode); //! //! impl AsXmlText for MyExitCode { //! fn as_xml_text(&self) -> Result, Error> { //! match self.0 { //! ExitCode::FAILURE => Ok(Cow::Borrowed("failure")), //! ExitCode::SUCCESS => Ok(Cow::Borrowed("success")), //! _ => Err(Error::Other("unknown exit code")), //! } //! } //! } //! //! impl FromXmlText for MyExitCode { //! fn from_xml_text(s: String) -> Result { //! match s.as_str() { //! "failure" => Ok(Self(ExitCode::FAILURE)), //! "success" => Ok(Self(ExitCode::SUCCESS)), //! _ => Err(Error::Other("unknown exit code")), //! } //! } //! } //! //! #[derive(AsXml, FromXml)] //! #[xml(namespace = "urn:example", name = "process-result")] //! struct ProcessResult { //! #[xml(attribute)] //! code: MyExitCode, //! #[xml(text)] //! stdout: String, //! } //! ``` //! //! Of course, such an approach reduces the usability of your struct (and //! comes with issues once references are needed), so making a merge request //! against `xso` is generally preferable. use core::marker::PhantomData; use alloc::{ borrow::{Cow, ToOwned}, boxed::Box, format, string::{String, ToString}, vec::Vec, }; use crate::{error::Error, AsOptionalXmlText, AsXmlText, FromXmlText}; #[cfg(feature = "base64")] use base64::engine::general_purpose::STANDARD as StandardBase64Engine; /// # Generate `AsXmlText` and `FromXmlText` implementations /// /// This macro generates an `AsXmlText` implementation which uses /// [`Display`][`core::fmt::Display`] and an `FromXmlText` which uses /// [`FromStr`][`core::str::FromStr`] for the types it is called on. /// /// ## Syntax /// /// The macro accepts a comma-separated list of types. Optionally, each type /// can be preceded by a `#[cfg(..)]` attribute to make the implementations /// conditional on a feature. /// /// ## Example /// #[cfg_attr( not(feature = "macros"), doc = "Because the macros feature was not enabled at doc build time, the example cannot be tested.\n\n```ignore\n" )] #[cfg_attr(feature = "macros", doc = "\n```\n")] /// # use xso::convert_via_fromstr_and_display; /// # use core::fmt::{self, Display}; /// # use core::str::FromStr; /// struct Foo; /// /// impl FromStr for Foo { /// # type Err = core::convert::Infallible; /// # /// # fn from_str(s: &str) -> Result { todo!() } /// /* ... */ /// } /// /// impl Display for Foo { /// # fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { todo!() } /// /* ... */ /// } /// /// convert_via_fromstr_and_display!( /// Foo, /// ); /// ``` #[macro_export] macro_rules! convert_via_fromstr_and_display { ($($(#[cfg $cfg:tt])?$t:ty),+ $(,)?) => { $( $( #[cfg $cfg] )? impl $crate::FromXmlText for $t { #[doc = concat!("Parse [`", stringify!($t), "`] from XML text via [`FromStr`][`core::str::FromStr`].")] fn from_xml_text(s: String) -> Result { s.parse().map_err($crate::error::Error::text_parse_error) } } $( #[cfg $cfg] )? impl $crate::AsXmlText for $t { #[doc = concat!("Convert [`", stringify!($t), "`] to XML text via [`Display`][`core::fmt::Display`].\n\nThis implementation never fails.")] fn as_xml_text(&self) -> Result<$crate::exports::alloc::borrow::Cow<'_, str>, $crate::error::Error> { Ok($crate::exports::alloc::borrow::Cow::Owned(self.to_string())) } } )+ } } /// This provides an implementation compliant with xsd::bool. impl FromXmlText for bool { /// Parse a boolean from XML text. /// /// The values `"1"` and `"true"` are considered true. The values `"0"` /// and `"false"` are considered `false`. Any other value is invalid and /// will return an error. fn from_xml_text(s: String) -> Result { match s.as_str() { "1" => "true", "0" => "false", other => other, } .parse() .map_err(Error::text_parse_error) } } /// This provides an implementation compliant with xsd::bool. impl AsXmlText for bool { /// Convert a boolean to XML text. /// /// `true` is converted to `"true"` and `false` is converted to `"false"`. /// This implementation never fails. fn as_xml_text(&self) -> Result, Error> { match self { true => Ok(Cow::Borrowed("true")), false => Ok(Cow::Borrowed("false")), } } } convert_via_fromstr_and_display! { u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, char, core::net::IpAddr, core::net::Ipv4Addr, core::net::Ipv6Addr, core::net::SocketAddr, core::net::SocketAddrV4, core::net::SocketAddrV6, core::num::NonZeroU8, core::num::NonZeroU16, core::num::NonZeroU32, core::num::NonZeroU64, core::num::NonZeroU128, core::num::NonZeroUsize, core::num::NonZeroI8, core::num::NonZeroI16, core::num::NonZeroI32, core::num::NonZeroI64, core::num::NonZeroI128, core::num::NonZeroIsize, #[cfg(feature = "uuid")] uuid::Uuid, #[cfg(feature = "jid")] jid::Jid, #[cfg(feature = "jid")] jid::FullJid, #[cfg(feature = "jid")] jid::BareJid, #[cfg(feature = "jid")] jid::NodePart, #[cfg(feature = "jid")] jid::DomainPart, #[cfg(feature = "jid")] jid::ResourcePart, #[cfg(feature = "serde_json")] serde_json::Value, } impl FromXmlText for String { /// Return the string unchanged. fn from_xml_text(data: String) -> Result { Ok(data) } } impl> FromXmlText for Cow<'_, B> { /// Return a [`Cow::Owned`] containing the parsed value. fn from_xml_text(data: String) -> Result { Ok(Cow::Owned(T::from_xml_text(data)?)) } } impl FromXmlText for Option { /// Return a [`Some`] containing the parsed value. fn from_xml_text(data: String) -> Result { Ok(Some(T::from_xml_text(data)?)) } } impl FromXmlText for Box { /// Return a [`Box`] containing the parsed value. fn from_xml_text(data: String) -> Result { Ok(Box::new(T::from_xml_text(data)?)) } } impl AsXmlText for String { /// Return the borrowed string contents. fn as_xml_text(&self) -> Result, Error> { Ok(Cow::Borrowed(self)) } } impl AsXmlText for str { /// Return the borrowed string contents. fn as_xml_text(&self) -> Result, Error> { Ok(Cow::Borrowed(self)) } } impl AsXmlText for &str { /// Return the borrowed string contents. fn as_xml_text(&self) -> Result, Error> { Ok(Cow::Borrowed(self)) } } impl AsXmlText for Box { /// Return the borrowed [`Box`] contents. fn as_xml_text(&self) -> Result, Error> { T::as_xml_text(self) } } impl AsXmlText for Cow<'_, B> { /// Return the borrowed [`Cow`] contents. fn as_xml_text(&self) -> Result, Error> { B::as_xml_text(self) } } impl AsXmlText for &T { /// Delegate to the `AsXmlText` implementation on `T`. fn as_xml_text(&self) -> Result, Error> { T::as_xml_text(*self) } } impl AsOptionalXmlText for T { fn as_optional_xml_text(&self) -> Result>, Error> { ::as_optional_xml_text(self) } } impl AsOptionalXmlText for Option { fn as_optional_xml_text(&self) -> Result>, Error> { self.as_ref() .map(T::as_optional_xml_text) .transpose() .map(Option::flatten) } } /// Represent a way to encode/decode text data into a Rust type. /// /// This trait can be used in scenarios where implementing [`FromXmlText`] /// and/or [`AsXmlText`] on a type is not feasible or sensible, such as the /// following: /// /// 1. The type originates in a foreign crate, preventing the implementation /// of foreign traits. /// /// 2. There is more than one way to convert a value to/from XML. /// /// The codec to use for a text can be specified in the attributes understood /// by `FromXml` and `AsXml` derive macros. See the documentation of the /// [`FromXml`][`macro@crate::FromXml`] derive macro for details. #[diagnostic::on_unimplemented( message = "`{Self}` cannot be used as XML text codec for values of type `{T}`." )] pub trait TextCodec { /// Decode a string value into the type. fn decode(&self, s: String) -> Result; /// Encode the type as string value. /// /// If this returns `None`, the string value is not emitted at all. fn encode<'x>(&self, value: &'x T) -> Result>, Error>; /// Apply a filter to this codec. /// /// Filters preprocess strings before they are handed to the codec for /// parsing, allowing to, for example, make the codec ignore irrelevant /// content by stripping it. // NOTE: The bound on T is needed because any given type A may implement // TextCodec for any number of types. If we pass T down to the `Filtered` // struct, rustc can do type inference on which `TextCodec` // implementation the `filtered` method is supposed to have been called // on. fn filtered(self, filter: F) -> Filtered where // placing the bound here (instead of on the `TextCodec` trait // itself) preserves object-safety of TextCodec. Self: Sized, { Filtered { filter, codec: self, bound: PhantomData, } } } /// Wrapper struct to apply a filter to a codec. /// /// You can construct a value of this type via [`TextCodec::filtered`]. // NOTE: see the note on TextCodec::filtered for why we bind `T` here, too. pub struct Filtered { filter: F, codec: C, bound: PhantomData, } impl> TextCodec for Filtered { fn decode(&self, s: String) -> Result { let s = self.filter.preprocess(s); self.codec.decode(s) } fn encode<'x>(&self, value: &'x T) -> Result>, Error> { self.codec.encode(value) } } /// Text codec which does no transform. pub struct Plain; impl TextCodec for Plain { fn decode(&self, s: String) -> Result { Ok(s) } fn encode<'x>(&self, value: &'x String) -> Result>, Error> { Ok(Some(Cow::Borrowed(value.as_str()))) } } /// Text codec which returns `None` if the input to decode is the empty string, instead of /// attempting to decode it. /// /// Particularly useful when parsing `Option` on `#[xml(text)]`, which does not support /// `Option<_>` otherwise. pub struct EmptyAsNone; impl TextCodec> for EmptyAsNone where T: FromXmlText + AsXmlText, { fn decode(&self, s: String) -> Result, Error> { if s.is_empty() { Ok(None) } else { Some(T::from_xml_text(s)).transpose() } } fn encode<'x>(&self, value: &'x Option) -> Result>, Error> { Ok(value .as_ref() .map(AsXmlText::as_xml_text) .transpose()? .and_then(|v| (!v.is_empty()).then_some(v))) } } /// Text codec which returns None instead of the empty string. pub struct EmptyAsError; impl TextCodec for EmptyAsError { fn decode(&self, s: String) -> Result { if s.is_empty() { Err(Error::Other("Empty text node.")) } else { Ok(s) } } fn encode<'x>(&self, value: &'x String) -> Result>, Error> { if value.is_empty() { Err(Error::Other("Empty text node.")) } else { Ok(Some(Cow::Borrowed(value.as_str()))) } } } /// Trait for preprocessing text data from XML. /// /// This may be used by codecs to allow to customize some of their behaviour. pub trait TextFilter { /// Process the incoming string and return the result of the processing. fn preprocess(&self, s: String) -> String; } /// Text preprocessor which returns the input unchanged. pub struct NoFilter; impl TextFilter for NoFilter { fn preprocess(&self, s: String) -> String { s } } /// Text preprocessor to remove all whitespace. pub struct StripWhitespace; impl TextFilter for StripWhitespace { fn preprocess(&self, s: String) -> String { let s: String = s .chars() .filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t') .collect(); s } } /// Text codec transforming text to binary using standard `base64`. /// /// `Base64` uses the [`base64::engine::general_purpose::STANDARD`] engine. /// [`TextCodec`] is also automatically implemented for any value which /// implements [`base64::engine::Engine`], allowing you to choose different /// alphabets easily. #[cfg(feature = "base64")] pub struct Base64; #[cfg(feature = "base64")] impl TextCodec> for Base64 { fn decode(&self, s: String) -> Result, Error> { base64::engine::Engine::decode(&StandardBase64Engine, s.as_bytes()) .map_err(Error::text_parse_error) } fn encode<'x>(&self, value: &'x Vec) -> Result>, Error> { Ok(Some(Cow::Owned(base64::engine::Engine::encode( &StandardBase64Engine, value, )))) } } #[cfg(feature = "base64")] impl<'x> TextCodec> for Base64 { fn decode(&self, s: String) -> Result, Error> { base64::engine::Engine::decode(&StandardBase64Engine, s.as_bytes()) .map_err(Error::text_parse_error) .map(Cow::Owned) } fn encode<'a>(&self, value: &'a Cow<'x, [u8]>) -> Result>, Error> { Ok(Some(Cow::Owned(base64::engine::Engine::encode( &StandardBase64Engine, value, )))) } } #[cfg(feature = "base64")] impl TextCodec> for Base64 where Base64: TextCodec, { fn decode(&self, s: String) -> Result, Error> { if s.is_empty() { return Ok(None); } Ok(Some(self.decode(s)?)) } fn encode<'x>(&self, decoded: &'x Option) -> Result>, Error> { decoded .as_ref() .map(|x| self.encode(x)) .transpose() .map(Option::flatten) } } #[cfg(feature = "base64")] impl TextCodec> for T { fn decode(&self, s: String) -> Result, Error> { base64::engine::Engine::decode(self, s.as_bytes()).map_err(Error::text_parse_error) } fn encode<'x>(&self, value: &'x Vec) -> Result>, Error> { Ok(Some(Cow::Owned(base64::engine::Engine::encode( self, value, )))) } } #[cfg(feature = "base64")] impl TextCodec> for T where T: TextCodec, { fn decode(&self, s: String) -> Result, Error> { if s.is_empty() { return Ok(None); } Ok(Some(TextCodec::decode(self, s)?)) } fn encode<'x>(&self, decoded: &'x Option) -> Result>, Error> { decoded .as_ref() .map(|x| TextCodec::encode(self, x)) .transpose() .map(Option::flatten) } } /// Text codec transforming text to binary using hexadecimal nibbles. /// /// The length must be known at compile-time. pub struct FixedHex; impl TextCodec<[u8; N]> for FixedHex { fn decode(&self, s: String) -> Result<[u8; N], Error> { if s.len() != 2 * N { return Err(Error::Other("Invalid length")); } let mut bytes = [0u8; N]; for i in 0..N { bytes[i] = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(Error::text_parse_error)?; } Ok(bytes) } fn encode<'x>(&self, value: &'x [u8; N]) -> Result>, Error> { let mut bytes = String::with_capacity(N * 2); for byte in value { bytes.extend(format!("{:02x}", byte).chars()); } Ok(Some(Cow::Owned(bytes))) } } impl TextCodec> for FixedHex where FixedHex: TextCodec, { fn decode(&self, s: String) -> Result, Error> { if s.is_empty() { return Ok(None); } Ok(Some(self.decode(s)?)) } fn encode<'x>(&self, decoded: &'x Option) -> Result>, Error> { decoded .as_ref() .map(|x| self.encode(x)) .transpose() .map(Option::flatten) } } /// Text codec for colon-separated bytes of uppercase hexadecimal. pub struct ColonSeparatedHex; impl TextCodec> for ColonSeparatedHex { fn decode(&self, s: String) -> Result, Error> { assert_eq!((s.len() + 1) % 3, 0); let mut bytes = Vec::with_capacity((s.len() + 1) / 3); for i in 0..(1 + s.len()) / 3 { let byte = u8::from_str_radix(&s[3 * i..3 * i + 2], 16).map_err(Error::text_parse_error)?; if 3 * i + 2 < s.len() { assert_eq!(&s[3 * i + 2..3 * i + 3], ":"); } bytes.push(byte); } Ok(bytes) } fn encode<'x>(&self, decoded: &'x Vec) -> Result>, Error> { // TODO: Super inefficient! let mut bytes = Vec::with_capacity(decoded.len()); for byte in decoded { bytes.push(format!("{:02X}", byte)); } Ok(Some(Cow::Owned(bytes.join(":")))) } }