1#![cfg_attr(
42 not(all(feature = "std", feature = "macros")),
43 doc = "Because the std or macros feature was not enabled at doc build time, the example cannot be tested.\n\n```ignore\n"
44)]
45#![cfg_attr(all(feature = "std", feature = "macros"), doc = "\n```\n")]
46use core::marker::PhantomData;
87
88use alloc::{
89 borrow::Cow,
90 format,
91 string::{String, ToString},
92 vec::Vec,
93};
94
95use crate::{error::Error, AsXmlText, FromXmlText};
96
97#[cfg(feature = "base64")]
98use base64::engine::general_purpose::STANDARD as StandardBase64Engine;
99
100#[cfg_attr(
115 not(feature = "macros"),
116 doc = "Because the macros feature was not enabled at doc build time, the example cannot be tested.\n\n```ignore\n"
117)]
118#[cfg_attr(feature = "macros", doc = "\n```\n")]
119#[macro_export]
141macro_rules! convert_via_fromstr_and_display {
142 ($($(#[cfg $cfg:tt])?$t:ty),+ $(,)?) => {
143 $(
144 $(
145 #[cfg $cfg]
146 )?
147 impl $crate::FromXmlText for $t {
148 #[doc = concat!("Parse [`", stringify!($t), "`] from XML text via [`FromStr`][`core::str::FromStr`].")]
149 fn from_xml_text(s: String) -> Result<Self, $crate::error::Error> {
150 s.parse().map_err($crate::error::Error::text_parse_error)
151 }
152 }
153
154 $(
155 #[cfg $cfg]
156 )?
157 impl $crate::AsXmlText for $t {
158 #[doc = concat!("Convert [`", stringify!($t), "`] to XML text via [`Display`][`core::fmt::Display`].\n\nThis implementation never fails.")]
159 fn as_xml_text(&self) -> Result<$crate::exports::alloc::borrow::Cow<'_, str>, $crate::error::Error> {
160 Ok($crate::exports::alloc::borrow::Cow::Owned(self.to_string()))
161 }
162 }
163 )+
164 }
165}
166
167impl FromXmlText for bool {
169 fn from_xml_text(s: String) -> Result<Self, Error> {
175 match s.as_str() {
176 "1" => "true",
177 "0" => "false",
178 other => other,
179 }
180 .parse()
181 .map_err(Error::text_parse_error)
182 }
183}
184
185impl AsXmlText for bool {
187 fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
192 match self {
193 true => Ok(Cow::Borrowed("true")),
194 false => Ok(Cow::Borrowed("false")),
195 }
196 }
197}
198
199convert_via_fromstr_and_display! {
200 u8,
201 u16,
202 u32,
203 u64,
204 u128,
205 usize,
206 i8,
207 i16,
208 i32,
209 i64,
210 i128,
211 isize,
212 f32,
213 f64,
214 char,
215 core::net::IpAddr,
216 core::net::Ipv4Addr,
217 core::net::Ipv6Addr,
218 core::net::SocketAddr,
219 core::net::SocketAddrV4,
220 core::net::SocketAddrV6,
221 core::num::NonZeroU8,
222 core::num::NonZeroU16,
223 core::num::NonZeroU32,
224 core::num::NonZeroU64,
225 core::num::NonZeroU128,
226 core::num::NonZeroUsize,
227 core::num::NonZeroI8,
228 core::num::NonZeroI16,
229 core::num::NonZeroI32,
230 core::num::NonZeroI64,
231 core::num::NonZeroI128,
232 core::num::NonZeroIsize,
233
234 #[cfg(feature = "uuid")]
235 uuid::Uuid,
236
237 #[cfg(feature = "jid")]
238 jid::Jid,
239 #[cfg(feature = "jid")]
240 jid::FullJid,
241 #[cfg(feature = "jid")]
242 jid::BareJid,
243 #[cfg(feature = "jid")]
244 jid::NodePart,
245 #[cfg(feature = "jid")]
246 jid::DomainPart,
247 #[cfg(feature = "jid")]
248 jid::ResourcePart,
249
250 #[cfg(feature = "serde_json")]
251 serde_json::Value,
252}
253
254#[diagnostic::on_unimplemented(
269 message = "`{Self}` cannot be used as XML text codec for values of type `{T}`."
270)]
271pub trait TextCodec<T> {
272 fn decode(&self, s: String) -> Result<T, Error>;
274
275 fn encode<'x>(&self, value: &'x T) -> Result<Option<Cow<'x, str>>, Error>;
279
280 fn filtered<F: TextFilter>(self, filter: F) -> Filtered<F, Self, T>
291 where
292 Self: Sized,
295 {
296 Filtered {
297 filter,
298 codec: self,
299 bound: PhantomData,
300 }
301 }
302}
303
304pub struct Filtered<F, C, T> {
309 filter: F,
310 codec: C,
311 bound: PhantomData<T>,
312}
313
314impl<T, F: TextFilter, C: TextCodec<T>> TextCodec<T> for Filtered<F, C, T> {
315 fn decode(&self, s: String) -> Result<T, Error> {
316 let s = self.filter.preprocess(s);
317 self.codec.decode(s)
318 }
319
320 fn encode<'x>(&self, value: &'x T) -> Result<Option<Cow<'x, str>>, Error> {
321 self.codec.encode(value)
322 }
323}
324
325pub struct Plain;
327
328impl TextCodec<String> for Plain {
329 fn decode(&self, s: String) -> Result<String, Error> {
330 Ok(s)
331 }
332
333 fn encode<'x>(&self, value: &'x String) -> Result<Option<Cow<'x, str>>, Error> {
334 Ok(Some(Cow::Borrowed(value.as_str())))
335 }
336}
337
338pub struct EmptyAsNone;
344
345impl<T> TextCodec<Option<T>> for EmptyAsNone
346where
347 T: FromXmlText + AsXmlText,
348{
349 fn decode(&self, s: String) -> Result<Option<T>, Error> {
350 if s.is_empty() {
351 Ok(None)
352 } else {
353 Some(T::from_xml_text(s)).transpose()
354 }
355 }
356
357 fn encode<'x>(&self, value: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
358 Ok(value
359 .as_ref()
360 .map(AsXmlText::as_xml_text)
361 .transpose()?
362 .and_then(|v| (!v.is_empty()).then_some(v)))
363 }
364}
365
366pub struct EmptyAsError;
368
369impl TextCodec<String> for EmptyAsError {
370 fn decode(&self, s: String) -> Result<String, Error> {
371 if s.is_empty() {
372 Err(Error::Other("Empty text node."))
373 } else {
374 Ok(s)
375 }
376 }
377
378 fn encode<'x>(&self, value: &'x String) -> Result<Option<Cow<'x, str>>, Error> {
379 if value.is_empty() {
380 Err(Error::Other("Empty text node."))
381 } else {
382 Ok(Some(Cow::Borrowed(value.as_str())))
383 }
384 }
385}
386
387pub trait TextFilter {
391 fn preprocess(&self, s: String) -> String;
393}
394
395pub struct NoFilter;
397
398impl TextFilter for NoFilter {
399 fn preprocess(&self, s: String) -> String {
400 s
401 }
402}
403
404pub struct StripWhitespace;
406
407impl TextFilter for StripWhitespace {
408 fn preprocess(&self, s: String) -> String {
409 let s: String = s
410 .chars()
411 .filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
412 .collect();
413 s
414 }
415}
416
417#[cfg(feature = "base64")]
424pub struct Base64;
425
426#[cfg(feature = "base64")]
427impl TextCodec<Vec<u8>> for Base64 {
428 fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
429 base64::engine::Engine::decode(&StandardBase64Engine, s.as_bytes())
430 .map_err(Error::text_parse_error)
431 }
432
433 fn encode<'x>(&self, value: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
434 Ok(Some(Cow::Owned(base64::engine::Engine::encode(
435 &StandardBase64Engine,
436 value,
437 ))))
438 }
439}
440
441#[cfg(feature = "base64")]
442impl<'x> TextCodec<Cow<'x, [u8]>> for Base64 {
443 fn decode(&self, s: String) -> Result<Cow<'x, [u8]>, Error> {
444 base64::engine::Engine::decode(&StandardBase64Engine, s.as_bytes())
445 .map_err(Error::text_parse_error)
446 .map(Cow::Owned)
447 }
448
449 fn encode<'a>(&self, value: &'a Cow<'x, [u8]>) -> Result<Option<Cow<'a, str>>, Error> {
450 Ok(Some(Cow::Owned(base64::engine::Engine::encode(
451 &StandardBase64Engine,
452 value,
453 ))))
454 }
455}
456
457#[cfg(feature = "base64")]
458impl<T> TextCodec<Option<T>> for Base64
459where
460 Base64: TextCodec<T>,
461{
462 fn decode(&self, s: String) -> Result<Option<T>, Error> {
463 if s.is_empty() {
464 return Ok(None);
465 }
466 Ok(Some(self.decode(s)?))
467 }
468
469 fn encode<'x>(&self, decoded: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
470 decoded
471 .as_ref()
472 .map(|x| self.encode(x))
473 .transpose()
474 .map(Option::flatten)
475 }
476}
477
478#[cfg(feature = "base64")]
479impl<T: base64::engine::Engine> TextCodec<Vec<u8>> for T {
480 fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
481 base64::engine::Engine::decode(self, s.as_bytes()).map_err(Error::text_parse_error)
482 }
483
484 fn encode<'x>(&self, value: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
485 Ok(Some(Cow::Owned(base64::engine::Engine::encode(
486 self, value,
487 ))))
488 }
489}
490
491#[cfg(feature = "base64")]
492impl<T: base64::engine::Engine, U> TextCodec<Option<U>> for T
493where
494 T: TextCodec<U>,
495{
496 fn decode(&self, s: String) -> Result<Option<U>, Error> {
497 if s.is_empty() {
498 return Ok(None);
499 }
500 Ok(Some(TextCodec::decode(self, s)?))
501 }
502
503 fn encode<'x>(&self, decoded: &'x Option<U>) -> Result<Option<Cow<'x, str>>, Error> {
504 decoded
505 .as_ref()
506 .map(|x| TextCodec::encode(self, x))
507 .transpose()
508 .map(Option::flatten)
509 }
510}
511
512pub struct FixedHex<const N: usize>;
516
517impl<const N: usize> TextCodec<[u8; N]> for FixedHex<N> {
518 fn decode(&self, s: String) -> Result<[u8; N], Error> {
519 if s.len() != 2 * N {
520 return Err(Error::Other("Invalid length"));
521 }
522
523 let mut bytes = [0u8; N];
524 for i in 0..N {
525 bytes[i] =
526 u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(Error::text_parse_error)?;
527 }
528
529 Ok(bytes)
530 }
531
532 fn encode<'x>(&self, value: &'x [u8; N]) -> Result<Option<Cow<'x, str>>, Error> {
533 let mut bytes = String::with_capacity(N * 2);
534 for byte in value {
535 bytes.extend(format!("{:02x}", byte).chars());
536 }
537 Ok(Some(Cow::Owned(bytes)))
538 }
539}
540
541impl<T, const N: usize> TextCodec<Option<T>> for FixedHex<N>
542where
543 FixedHex<N>: TextCodec<T>,
544{
545 fn decode(&self, s: String) -> Result<Option<T>, Error> {
546 if s.is_empty() {
547 return Ok(None);
548 }
549 Ok(Some(self.decode(s)?))
550 }
551
552 fn encode<'x>(&self, decoded: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
553 decoded
554 .as_ref()
555 .map(|x| self.encode(x))
556 .transpose()
557 .map(Option::flatten)
558 }
559}
560
561pub struct ColonSeparatedHex;
563
564impl TextCodec<Vec<u8>> for ColonSeparatedHex {
565 fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
566 assert_eq!((s.len() + 1) % 3, 0);
567 let mut bytes = Vec::with_capacity((s.len() + 1) / 3);
568 for i in 0..(1 + s.len()) / 3 {
569 let byte =
570 u8::from_str_radix(&s[3 * i..3 * i + 2], 16).map_err(Error::text_parse_error)?;
571 if 3 * i + 2 < s.len() {
572 assert_eq!(&s[3 * i + 2..3 * i + 3], ":");
573 }
574 bytes.push(byte);
575 }
576 Ok(bytes)
577 }
578
579 fn encode<'x>(&self, decoded: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
580 let mut bytes = Vec::with_capacity(decoded.len());
582 for byte in decoded {
583 bytes.push(format!("{:02X}", byte));
584 }
585 Ok(Some(Cow::Owned(bytes.join(":"))))
586 }
587}