xso: add text conversion traits for AsXml

Jonas Schäfer created

Change summary

xso/src/lib.rs  | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++
xso/src/text.rs | 24 +++++++++++++
2 files changed, 107 insertions(+), 1 deletion(-)

Detailed changes

xso/src/lib.rs 🔗

@@ -285,6 +285,90 @@ impl<T: IntoOptionalXmlText> IntoOptionalXmlText for Option<T> {
     }
 }
 
+/// 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 AsXmlText {
+    /// Convert the value to an XML string in a context where an absent value
+    /// cannot be represented.
+    fn as_xml_text(&self) -> Result<Cow<'_, str>, self::error::Error>;
+
+    /// 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::as_xml_text`] wrapped into `Some(.)`. By re-implementing
+    /// this method, implementors can customize the behaviour for certain
+    /// values.
+    fn as_optional_xml_text(&self) -> Result<Option<Cow<'_, str>>, self::error::Error> {
+        Ok(Some(self.as_xml_text()?))
+    }
+}
+
+impl AsXmlText for String {
+    fn as_xml_text(&self) -> Result<Cow<'_, str>, self::error::Error> {
+        Ok(Cow::Borrowed(self.as_str()))
+    }
+}
+
+impl AsXmlText for &str {
+    fn as_xml_text(&self) -> Result<Cow<'_, str>, self::error::Error> {
+        Ok(Cow::Borrowed(&**self))
+    }
+}
+
+impl<T: AsXmlText> AsXmlText for Box<T> {
+    fn as_xml_text(&self) -> Result<Cow<'_, str>, self::error::Error> {
+        T::as_xml_text(&*self)
+    }
+}
+
+impl<B: AsXmlText + ToOwned> AsXmlText for Cow<'_, B> {
+    fn as_xml_text(&self) -> Result<Cow<'_, str>, self::error::Error> {
+        B::as_xml_text(self.as_ref())
+    }
+}
+
+/// Specialized variant of [`AsXmlText`].
+///
+/// Do **not** implement this unless you cannot implement [`AsXmlText`]:
+/// implementing [`AsXmlText`] is more versatile and an
+/// [`AsOptionalXmlText`] implementation is automatically provided.
+///
+/// If you need to customize the behaviour of the [`AsOptionalXmlText`]
+/// blanket implementation, implement a custom
+/// [`AsXmlText::as_optional_xml_text`] instead.
+pub trait AsOptionalXmlText {
+    /// Convert the value to an XML string in a context where an absent value
+    /// can be represented.
+    fn as_optional_xml_text(&self) -> Result<Option<Cow<'_, str>>, self::error::Error>;
+}
+
+impl<T: AsXmlText> AsOptionalXmlText for T {
+    fn as_optional_xml_text(&self) -> Result<Option<Cow<'_, str>>, self::error::Error> {
+        <Self as AsXmlText>::as_optional_xml_text(self)
+    }
+}
+
+impl<T: AsXmlText> AsOptionalXmlText for Option<T> {
+    fn as_optional_xml_text(&self) -> Result<Option<Cow<'_, str>>, self::error::Error> {
+        self.as_ref()
+            .map(T::as_optional_xml_text)
+            .transpose()
+            .map(Option::flatten)
+    }
+}
+
 /// Attempt to transform a type implementing [`IntoXml`] into another
 /// type which implements [`FromXml`].
 pub fn transform<T: FromXml, F: IntoXml>(from: F) -> Result<T, self::error::Error> {

xso/src/text.rs 🔗

@@ -9,7 +9,9 @@
 #[cfg(feature = "base64")]
 use core::marker::PhantomData;
 
-use crate::{error::Error, FromXmlText, IntoXmlText};
+use std::borrow::Cow;
+
+use crate::{error::Error, AsXmlText, FromXmlText, IntoXmlText};
 
 #[cfg(feature = "base64")]
 use base64::engine::{general_purpose::STANDARD as StandardBase64Engine, Engine as _};
@@ -40,6 +42,16 @@ macro_rules! convert_via_fromstr_and_display {
                     Ok(self.to_string())
                 }
             }
+
+            $(
+                #[cfg(feature = $feature)]
+                #[cfg_attr(docsrs, doc(cfg(feature = $feature)))]
+            )?
+            impl AsXmlText for $t {
+                fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
+                    Ok(Cow::Owned(self.to_string()))
+                }
+            }
         )+
     }
 }
@@ -64,6 +76,16 @@ impl IntoXmlText for bool {
     }
 }
 
+/// This provides an implementation compliant with xsd::bool.
+impl AsXmlText for bool {
+    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
+        match self {
+            true => Ok(Cow::Borrowed("true")),
+            false => Ok(Cow::Borrowed("false")),
+        }
+    }
+}
+
 convert_via_fromstr_and_display! {
     u8,
     u16,