text.rs

  1// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7//! Module containing implementations for conversions to/from XML text.
  8
  9#[cfg(feature = "base64")]
 10use core::marker::PhantomData;
 11
 12use std::borrow::Cow;
 13
 14use crate::{error::Error, AsXmlText, FromXmlText};
 15
 16#[cfg(feature = "base64")]
 17use base64::engine::{general_purpose::STANDARD as StandardBase64Engine, Engine as _};
 18#[cfg(feature = "jid")]
 19use jid;
 20#[cfg(feature = "uuid")]
 21use uuid;
 22
 23macro_rules! convert_via_fromstr_and_display {
 24    ($($(#[cfg(feature = $feature:literal)])?$t:ty,)+) => {
 25        $(
 26            $(
 27                #[cfg(feature = $feature)]
 28                #[cfg_attr(docsrs, doc(cfg(feature = $feature)))]
 29            )?
 30            impl FromXmlText for $t {
 31                fn from_xml_text(s: String) -> Result<Self, Error> {
 32                    s.parse().map_err(Error::text_parse_error)
 33                }
 34            }
 35
 36            $(
 37                #[cfg(feature = $feature)]
 38                #[cfg_attr(docsrs, doc(cfg(feature = $feature)))]
 39            )?
 40            impl AsXmlText for $t {
 41                fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
 42                    Ok(Cow::Owned(self.to_string()))
 43                }
 44            }
 45        )+
 46    }
 47}
 48
 49/// This provides an implementation compliant with xsd::bool.
 50impl FromXmlText for bool {
 51    fn from_xml_text(s: String) -> Result<Self, Error> {
 52        match s.as_str() {
 53            "1" => "true",
 54            "0" => "false",
 55            other => other,
 56        }
 57        .parse()
 58        .map_err(Error::text_parse_error)
 59    }
 60}
 61
 62/// This provides an implementation compliant with xsd::bool.
 63impl AsXmlText for bool {
 64    fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
 65        match self {
 66            true => Ok(Cow::Borrowed("true")),
 67            false => Ok(Cow::Borrowed("false")),
 68        }
 69    }
 70}
 71
 72convert_via_fromstr_and_display! {
 73    u8,
 74    u16,
 75    u32,
 76    u64,
 77    u128,
 78    usize,
 79    i8,
 80    i16,
 81    i32,
 82    i64,
 83    i128,
 84    isize,
 85    f32,
 86    f64,
 87    std::net::IpAddr,
 88    std::net::Ipv4Addr,
 89    std::net::Ipv6Addr,
 90    std::net::SocketAddr,
 91    std::net::SocketAddrV4,
 92    std::net::SocketAddrV6,
 93    std::num::NonZeroU8,
 94    std::num::NonZeroU16,
 95    std::num::NonZeroU32,
 96    std::num::NonZeroU64,
 97    std::num::NonZeroU128,
 98    std::num::NonZeroUsize,
 99    std::num::NonZeroI8,
100    std::num::NonZeroI16,
101    std::num::NonZeroI32,
102    std::num::NonZeroI64,
103    std::num::NonZeroI128,
104    std::num::NonZeroIsize,
105
106    #[cfg(feature = "uuid")]
107    uuid::Uuid,
108
109    #[cfg(feature = "jid")]
110    jid::Jid,
111    #[cfg(feature = "jid")]
112    jid::FullJid,
113    #[cfg(feature = "jid")]
114    jid::BareJid,
115}
116
117/// Represent a way to encode/decode text data into a Rust type.
118///
119/// This trait can be used in scenarios where implementing [`FromXmlText`]
120/// and/or [`AsXmlText`] on a type is not feasible or sensible, such as the
121/// following:
122///
123/// 1. The type originates in a foreign crate, preventing the implementation
124///    of foreign traits.
125///
126/// 2. There is more than one way to convert a value to/from XML.
127///
128/// The codec to use for a text can be specified in the attributes understood
129/// by `FromXml` and `AsXml` derive macros. See the documentation of the
130/// [`FromXml`][`macro@crate::FromXml`] derive macro for details.
131pub trait TextCodec<T> {
132    /// Decode a string value into the type.
133    fn decode(s: String) -> Result<T, Error>;
134
135    /// Encode the type as string value.
136    ///
137    /// If this returns `None`, the string value is not emitted at all.
138    fn encode(value: &T) -> Result<Option<Cow<'_, str>>, Error>;
139}
140
141/// Text codec which does no transform.
142pub struct Plain;
143
144impl TextCodec<String> for Plain {
145    fn decode(s: String) -> Result<String, Error> {
146        Ok(s)
147    }
148
149    fn encode(value: &String) -> Result<Option<Cow<'_, str>>, Error> {
150        Ok(Some(Cow::Borrowed(value.as_str())))
151    }
152}
153
154/// Text codec which returns None instead of the empty string.
155pub struct EmptyAsNone;
156
157impl TextCodec<Option<String>> for EmptyAsNone {
158    fn decode(s: String) -> Result<Option<String>, Error> {
159        if s.is_empty() {
160            Ok(None)
161        } else {
162            Ok(Some(s))
163        }
164    }
165
166    fn encode(value: &Option<String>) -> Result<Option<Cow<'_, str>>, Error> {
167        Ok(match value.as_ref() {
168            Some(v) if !v.is_empty() => Some(Cow::Borrowed(v.as_str())),
169            Some(_) | None => None,
170        })
171    }
172}
173
174/// Trait for preprocessing text data from XML.
175///
176/// This may be used by codecs to allow to customize some of their behaviour.
177pub trait TextFilter {
178    /// Process the incoming string and return the result of the processing.
179    fn preprocess(s: String) -> String;
180}
181
182/// Text preprocessor which returns the input unchanged.
183pub struct NoFilter;
184
185impl TextFilter for NoFilter {
186    fn preprocess(s: String) -> String {
187        s
188    }
189}
190
191/// Text preprocessor to remove all whitespace.
192pub struct StripWhitespace;
193
194impl TextFilter for StripWhitespace {
195    fn preprocess(s: String) -> String {
196        let s: String = s
197            .chars()
198            .filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
199            .collect();
200        s
201    }
202}
203
204/// Text codec transforming text to binary using standard base64.
205///
206/// The `Filter` type argument can be used to employ additional preprocessing
207/// of incoming text data. Most interestingly, passing [`StripWhitespace`]
208/// will make the implementation ignore any whitespace within the text.
209#[cfg(feature = "base64")]
210#[cfg_attr(docsrs, doc(cfg(feature = "base64")))]
211pub struct Base64<Filter: TextFilter = NoFilter>(PhantomData<Filter>);
212
213#[cfg(feature = "base64")]
214#[cfg_attr(docsrs, doc(cfg(feature = "base64")))]
215impl<Filter: TextFilter> TextCodec<Vec<u8>> for Base64<Filter> {
216    fn decode(s: String) -> Result<Vec<u8>, Error> {
217        let value = Filter::preprocess(s);
218        StandardBase64Engine
219            .decode(value.as_bytes())
220            .map_err(Error::text_parse_error)
221    }
222
223    fn encode(value: &Vec<u8>) -> Result<Option<Cow<'_, str>>, Error> {
224        Ok(Some(Cow::Owned(StandardBase64Engine.encode(&value))))
225    }
226}
227
228#[cfg(feature = "base64")]
229#[cfg_attr(docsrs, doc(cfg(feature = "base64")))]
230impl<Filter: TextFilter> TextCodec<Option<Vec<u8>>> for Base64<Filter> {
231    fn decode(s: String) -> Result<Option<Vec<u8>>, Error> {
232        if s.is_empty() {
233            return Ok(None);
234        }
235        Ok(Some(Self::decode(s)?))
236    }
237
238    fn encode(decoded: &Option<Vec<u8>>) -> Result<Option<Cow<'_, str>>, Error> {
239        decoded
240            .as_ref()
241            .map(Self::encode)
242            .transpose()
243            .map(Option::flatten)
244    }
245}