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}