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