diff --git a/xso/Cargo.toml b/xso/Cargo.toml index bc0036e80f41bee70f048e4536d4a18840c5f47e..b7d7957a66b28ee90d65f78a60b9a65d0d4c595e 100644 --- a/xso/Cargo.toml +++ b/xso/Cargo.toml @@ -14,6 +14,14 @@ rxml = { version = "0.11.0", default-features = false } minidom = { version = "^0.15" } xso_proc = { version = "0.0.2", optional = true } +# optional dependencies to provide text conversion to/from types from these crates +# NOTE: because we don't have public/private dependencies yet and cargo +# defaults to picking the highest matching version by default, the only +# sensible thing we can do here is to depend on the least version of the most +# recent semver of each crate. +jid = { version = "^0.10", optional = true } +uuid = { version = "^1", optional = true } + [features] macros = [ "dep:xso_proc" ] minidom = [ "xso_proc/minidom"] diff --git a/xso/src/lib.rs b/xso/src/lib.rs index b6874b356483ca2058f7ae42b71ea5bd9514f389..dc484d4ca51718eccf85b93dd2c8b0104b8b37ad 100644 --- a/xso/src/lib.rs +++ b/xso/src/lib.rs @@ -1,3 +1,4 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] #![forbid(unsafe_code)] #![warn(missing_docs)] /*! @@ -22,6 +23,7 @@ use of this library in parsing XML streams like specified in RFC 6120. pub mod error; #[cfg(feature = "minidom")] pub mod minidom_compat; +mod text; #[doc(hidden)] pub mod exports { @@ -30,6 +32,8 @@ pub mod exports { pub use rxml; } +use std::borrow::Cow; + #[doc = include_str!("from_xml_doc.md")] #[doc(inline)] #[cfg(feature = "macros")] @@ -130,6 +134,126 @@ pub trait FromXml { ) -> Result; } +/// Trait allowing to convert XML text to a value. +/// +/// This trait is similar to [`std::str::FromStr`], however, due to +/// restrictions imposed by the orphan rule, a separate trait is needed. +/// Implementations for many standard library types are available. In +/// addition, the following feature flags can enable more implementations: +/// +/// - `jid`: `jid::Jid`, `jid::BareJid`, `jid::FullJid` +/// - `uuid`: `uuid::Uuid` +/// +/// Because of this unfortunate situation, we are **extremely liberal** with +/// accepting optional dependencies for this purpose. You are very welcome to +/// make merge requests against this crate adding support for parsing +/// third-party crates. +pub trait FromXmlText: Sized { + /// Convert the given XML text to a value. + fn from_xml_text(data: String) -> Result; +} + +impl FromXmlText for String { + fn from_xml_text(data: String) -> Result { + Ok(data) + } +} + +impl> FromXmlText for Cow<'_, B> { + fn from_xml_text(data: String) -> Result { + Ok(Cow::Owned(T::from_xml_text(data)?)) + } +} + +impl FromXmlText for Option { + fn from_xml_text(data: String) -> Result { + Ok(Some(T::from_xml_text(data)?)) + } +} + +impl FromXmlText for Box { + fn from_xml_text(data: String) -> Result { + Ok(Box::new(T::from_xml_text(data)?)) + } +} + +/// Trait to convert a value to an XML text string. +/// +/// This trait is implemented for many standard library types implementing +/// [`std::fmt::Display`]. In addition, the following feature flags can enable +/// more implementations: +/// +/// - `jid`: `jid::Jid`, `jid::BareJid`, `jid::FullJid` +/// - `uuid`: `uuid::Uuid` +/// +/// Because of the unfortunate situation as described in [`FromXmlText`], we +/// are **extremely liberal** with accepting optional dependencies for this +/// purpose. You are very welcome to make merge requests against this crate +/// adding support for parsing third-party crates. +pub trait IntoXmlText: Sized { + /// Convert the value to an XML string in a context where an absent value + /// cannot be represented. + fn into_xml_text(self) -> Result; + + /// Convert the value to an XML string in a context where an absent value + /// can be represented. + /// + /// The provided implementation will always return the result of + /// [`Self::into_xml_text`] wrapped into `Some(.)`. By re-implementing + /// this method, implementors can customize the behaviour for certain + /// values. + fn into_optional_xml_text(self) -> Result, self::error::Error> { + Ok(Some(self.into_xml_text()?)) + } +} + +impl IntoXmlText for String { + fn into_xml_text(self) -> Result { + Ok(self) + } +} + +impl IntoXmlText for Box { + fn into_xml_text(self) -> Result { + T::into_xml_text(*self) + } +} + +impl> IntoXmlText for Cow<'_, B> { + fn into_xml_text(self) -> Result { + T::into_xml_text(self.into_owned()) + } +} + +/// Specialized variant of [`IntoXmlText`]. +/// +/// Do **not** implement this unless you cannot implement [`IntoXmlText`]: +/// implementing [`IntoXmlText`] is more versatile and an +/// [`IntoOptionalXmlText`] implementation is automatically provided. +/// +/// If you need to customize the behaviour of the [`IntoOptionalXmlText`] +/// blanket implementation, implement a custom +/// [`IntoXmlText::into_optional_xml_text`] instead. +pub trait IntoOptionalXmlText { + /// Convert the value to an XML string in a context where an absent value + /// can be represented. + fn into_optional_xml_text(self) -> Result, self::error::Error>; +} + +impl IntoOptionalXmlText for T { + fn into_optional_xml_text(self) -> Result, self::error::Error> { + ::into_optional_xml_text(self) + } +} + +impl IntoOptionalXmlText for Option { + fn into_optional_xml_text(self) -> Result, self::error::Error> { + self.map(T::into_optional_xml_text) + .transpose() + .map(Option::flatten) + } +} + /// Attempt to transform a type implementing [`IntoXml`] into another /// type which implements [`FromXml`]. pub fn transform(from: F) -> Result { diff --git a/xso/src/text.rs b/xso/src/text.rs new file mode 100644 index 0000000000000000000000000000000000000000..bdacd1518a762f3c3693f17a1f7f6728beee30cd --- /dev/null +++ b/xso/src/text.rs @@ -0,0 +1,105 @@ +// 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/. + +//! Module containing implementations for conversions to/from XML text. + +use crate::{error::Error, FromXmlText, IntoXmlText}; + +#[cfg(feature = "jid")] +use jid; +#[cfg(feature = "uuid")] +use uuid; + +macro_rules! convert_via_fromstr_and_display { + ($($(#[cfg(feature = $feature:literal)])?$t:ty,)+) => { + $( + $( + #[cfg(feature = $feature)] + #[cfg_attr(docsrs, doc(cfg(feature = $feature)))] + )? + impl FromXmlText for $t { + fn from_xml_text(s: String) -> Result { + s.parse().map_err(Error::text_parse_error) + } + } + + $( + #[cfg(feature = $feature)] + #[cfg_attr(docsrs, doc(cfg(feature = $feature)))] + )? + impl IntoXmlText for $t { + fn into_xml_text(self) -> Result { + Ok(self.to_string()) + } + } + )+ + } +} + +/// This provides an implementation compliant with xsd::bool. +impl FromXmlText for bool { + 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 IntoXmlText for bool { + fn into_xml_text(self) -> Result { + Ok(self.to_string()) + } +} + +convert_via_fromstr_and_display! { + u8, + u16, + u32, + u64, + u128, + usize, + i8, + i16, + i32, + i64, + i128, + isize, + f32, + f64, + std::net::IpAddr, + std::net::Ipv4Addr, + std::net::Ipv6Addr, + std::net::SocketAddr, + std::net::SocketAddrV4, + std::net::SocketAddrV6, + std::num::NonZeroU8, + std::num::NonZeroU16, + std::num::NonZeroU32, + std::num::NonZeroU64, + std::num::NonZeroU128, + std::num::NonZeroUsize, + std::num::NonZeroI8, + std::num::NonZeroI16, + std::num::NonZeroI32, + std::num::NonZeroI64, + std::num::NonZeroI128, + std::num::NonZeroIsize, + + #[cfg(feature = "uuid")] + uuid::Uuid, + + #[cfg(feature = "jid")] + jid::Jid, + #[cfg(feature = "jid")] + jid::FullJid, + #[cfg(feature = "jid")] + jid::BareJid, +}