xso: implement TextCodec<_> on all T: base64::engine::Engine

Jonas Schäfer created

The `xso::text::Base64` struct remains as a shorthand (because frankly,
I find the const names in the base64 crate very unwieldly), but you can
now use any of the base64 engines as codec.

Change summary

xso/ChangeLog   |  2 +
xso/src/text.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++------
2 files changed, 53 insertions(+), 8 deletions(-)

Detailed changes

xso/ChangeLog 🔗

@@ -27,6 +27,8 @@ Version NEXT:
         jid::DomainPart, and jid::ResourcePart (!485)
       - Support for optional child elements, the presence of which are
         translated into a boolean (`#[xml(flag)]`).
+      - Generic TextCodec implementation for all base64 engines provided by
+        the base64 crate (if the `base64` feature is enabled).
 
 Version 0.1.2:
 2024-07-26 Jonas Schäfer <jonas@zombofant.net>

xso/src/text.rs 🔗

@@ -18,7 +18,7 @@ use alloc::{
 use crate::{error::Error, AsXmlText, FromXmlText};
 
 #[cfg(feature = "base64")]
-use base64::engine::{general_purpose::STANDARD as StandardBase64Engine, Engine as _};
+use base64::engine::general_purpose::STANDARD as StandardBase64Engine;
 
 macro_rules! convert_via_fromstr_and_display {
     ($($(#[cfg $cfg:tt])?$t:ty,)+) => {
@@ -291,38 +291,47 @@ impl TextFilter for StripWhitespace {
     }
 }
 
-/// Text codec transforming text to binary using standard base64.
+/// Text codec transforming text to binary using standard `base64`.
 ///
 /// The `Filter` type argument can be used to employ additional preprocessing
 /// of incoming text data. Most interestingly, passing [`StripWhitespace`]
 /// will make the implementation ignore any whitespace within the text.
+///
+/// `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<Vec<u8>> for Base64 {
     fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
-        StandardBase64Engine
-            .decode(s.as_bytes())
+        base64::engine::Engine::decode(&StandardBase64Engine, s.as_bytes())
             .map_err(Error::text_parse_error)
     }
 
     fn encode<'x>(&self, value: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
-        Ok(Some(Cow::Owned(StandardBase64Engine.encode(&value))))
+        Ok(Some(Cow::Owned(base64::engine::Engine::encode(
+            &StandardBase64Engine,
+            &value,
+        ))))
     }
 }
 
 #[cfg(feature = "base64")]
 impl<'x> TextCodec<Cow<'x, [u8]>> for Base64 {
     fn decode(&self, s: String) -> Result<Cow<'x, [u8]>, Error> {
-        StandardBase64Engine
-            .decode(s.as_bytes())
+        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<Option<Cow<'a, str>>, Error> {
-        Ok(Some(Cow::Owned(StandardBase64Engine.encode(&value))))
+        Ok(Some(Cow::Owned(base64::engine::Engine::encode(
+            &StandardBase64Engine,
+            &value,
+        ))))
     }
 }
 
@@ -347,6 +356,40 @@ where
     }
 }
 
+#[cfg(feature = "base64")]
+impl<T: base64::engine::Engine> TextCodec<Vec<u8>> for T {
+    fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
+        base64::engine::Engine::decode(self, s.as_bytes()).map_err(Error::text_parse_error)
+    }
+
+    fn encode<'x>(&self, value: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
+        Ok(Some(Cow::Owned(base64::engine::Engine::encode(
+            self, &value,
+        ))))
+    }
+}
+
+#[cfg(feature = "base64")]
+impl<'a, T: base64::engine::Engine, U> TextCodec<Option<U>> for T
+where
+    T: TextCodec<U>,
+{
+    fn decode(&self, s: String) -> Result<Option<U>, Error> {
+        if s.is_empty() {
+            return Ok(None);
+        }
+        Ok(Some(TextCodec::decode(self, s)?))
+    }
+
+    fn encode<'x>(&self, decoded: &'x Option<U>) -> Result<Option<Cow<'x, str>>, 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.