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}