xmpp-rs/xso/src/text.rs
Jonas Schäfer 8c7b503f12 xso: reorganise the code a little
The main lib.rs is getting a bit cluttered, so I'm trying to bring some
order into the chaos by moving some things into other modules.

skip-changelog, because there are no user-facing changes (names which
have been moved are doc(inline)'d and pub use'd in the main lib, and
other things (trait implementations) aren't addressable by users).
2025-05-06 07:42:57 +02:00

673 lines
20 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//! # Convert data to and from XML text
//!
//! This module provides traits and types related to conversion of XML text
//! data to and from Rust types, as well as the [`AsXmlText`],
//! [`AsOptionalXmlText`][`crate::AsOptionalXmlText`] and [`FromXmlText`]
//! implementations for foreign and standard-library types.
//!
//! ## Support for types from third-party crates
//!
//! Beyond the standard library types, the following additional types are
//! supported:
//!
//! | Feature gate | Types |
//! | --- | --- |
//! | `jid` | `jid::Jid`, `jid::BareJid`, `jid::FullJid` |
//! | `serde_json` | `serde_json::Value` |
//! | `uuid` | `uuid::Uuid` |
//!
//! ### Adding support for more types
//!
//! Due to the orphan rule, it is not possible for applications to implement
//! [`AsXmlText`], [`AsOptionalXmlText`][`crate::AsOptionalXmlText`] or
//! [`FromXmlText`] on types which originate from third-party crates. Because
//! of that, we are **extremely liberal** at accepting merge requests for
//! implementations of these traits for types from third-party crates.
//!
//! The only requirement is that the implementation is gated behind a feature
//! flag which is disabled-by-default.
//!
//! ### Workaround for unsupported types
//!
//! If making a merge request against `xso` and waiting for a release is not
//! an option, you can use newtype wrappers in almost all cases, for example:
//!
#![cfg_attr(
not(all(feature = "std", feature = "macros")),
doc = "Because the std or macros feature was not enabled at doc build time, the example cannot be tested.\n\n```ignore\n"
)]
#![cfg_attr(all(feature = "std", feature = "macros"), doc = "\n```\n")]
//! # use xso::{AsXml, FromXml, AsXmlText, FromXmlText, error::Error};
//! # use std::borrow::Cow;
//! use std::process::ExitCode;
//!
//! struct MyExitCode(ExitCode);
//!
//! impl AsXmlText for MyExitCode {
//! fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
//! match self.0 {
//! ExitCode::FAILURE => Ok(Cow::Borrowed("failure")),
//! ExitCode::SUCCESS => Ok(Cow::Borrowed("success")),
//! _ => Err(Error::Other("unknown exit code")),
//! }
//! }
//! }
//!
//! impl FromXmlText for MyExitCode {
//! fn from_xml_text(s: String) -> Result<Self, Error> {
//! match s.as_str() {
//! "failure" => Ok(Self(ExitCode::FAILURE)),
//! "success" => Ok(Self(ExitCode::SUCCESS)),
//! _ => Err(Error::Other("unknown exit code")),
//! }
//! }
//! }
//!
//! #[derive(AsXml, FromXml)]
//! #[xml(namespace = "urn:example", name = "process-result")]
//! struct ProcessResult {
//! #[xml(attribute)]
//! code: MyExitCode,
//! #[xml(text)]
//! stdout: String,
//! }
//! ```
//!
//! Of course, such an approach reduces the usability of your struct (and
//! comes with issues once references are needed), so making a merge request
//! against `xso` is generally preferable.
use core::marker::PhantomData;
use alloc::{
borrow::{Cow, ToOwned},
boxed::Box,
format,
string::{String, ToString},
vec::Vec,
};
use crate::{error::Error, AsOptionalXmlText, AsXmlText, FromXmlText};
#[cfg(feature = "base64")]
use base64::engine::general_purpose::STANDARD as StandardBase64Engine;
/// # Generate `AsXmlText` and `FromXmlText` implementations
///
/// This macro generates an `AsXmlText` implementation which uses
/// [`Display`][`core::fmt::Display`] and an `FromXmlText` which uses
/// [`FromStr`][`core::str::FromStr`] for the types it is called on.
///
/// ## Syntax
///
/// The macro accepts a comma-separated list of types. Optionally, each type
/// can be preceded by a `#[cfg(..)]` attribute to make the implementations
/// conditional on a feature.
///
/// ## Example
///
#[cfg_attr(
not(feature = "macros"),
doc = "Because the macros feature was not enabled at doc build time, the example cannot be tested.\n\n```ignore\n"
)]
#[cfg_attr(feature = "macros", doc = "\n```\n")]
/// # use xso::convert_via_fromstr_and_display;
/// # use core::fmt::{self, Display};
/// # use core::str::FromStr;
/// struct Foo;
///
/// impl FromStr for Foo {
/// # type Err = core::convert::Infallible;
/// #
/// # fn from_str(s: &str) -> Result<Self, Self::Err> { todo!() }
/// /* ... */
/// }
///
/// impl Display for Foo {
/// # fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { todo!() }
/// /* ... */
/// }
///
/// convert_via_fromstr_and_display!(
/// Foo,
/// );
/// ```
#[macro_export]
macro_rules! convert_via_fromstr_and_display {
($($(#[cfg $cfg:tt])?$t:ty),+ $(,)?) => {
$(
$(
#[cfg $cfg]
)?
impl $crate::FromXmlText for $t {
#[doc = concat!("Parse [`", stringify!($t), "`] from XML text via [`FromStr`][`core::str::FromStr`].")]
fn from_xml_text(s: String) -> Result<Self, $crate::error::Error> {
s.parse().map_err($crate::error::Error::text_parse_error)
}
}
$(
#[cfg $cfg]
)?
impl $crate::AsXmlText for $t {
#[doc = concat!("Convert [`", stringify!($t), "`] to XML text via [`Display`][`core::fmt::Display`].\n\nThis implementation never fails.")]
fn as_xml_text(&self) -> Result<$crate::exports::alloc::borrow::Cow<'_, str>, $crate::error::Error> {
Ok($crate::exports::alloc::borrow::Cow::Owned(self.to_string()))
}
}
)+
}
}
/// This provides an implementation compliant with xsd::bool.
impl FromXmlText for bool {
/// Parse a boolean from XML text.
///
/// The values `"1"` and `"true"` are considered true. The values `"0"`
/// and `"false"` are considered `false`. Any other value is invalid and
/// will return an error.
fn from_xml_text(s: String) -> Result<Self, Error> {
match s.as_str() {
"1" => "true",
"0" => "false",
other => other,
}
.parse()
.map_err(Error::text_parse_error)
}
}
/// This provides an implementation compliant with xsd::bool.
impl AsXmlText for bool {
/// Convert a boolean to XML text.
///
/// `true` is converted to `"true"` and `false` is converted to `"false"`.
/// This implementation never fails.
fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
match self {
true => Ok(Cow::Borrowed("true")),
false => Ok(Cow::Borrowed("false")),
}
}
}
convert_via_fromstr_and_display! {
u8,
u16,
u32,
u64,
u128,
usize,
i8,
i16,
i32,
i64,
i128,
isize,
f32,
f64,
char,
core::net::IpAddr,
core::net::Ipv4Addr,
core::net::Ipv6Addr,
core::net::SocketAddr,
core::net::SocketAddrV4,
core::net::SocketAddrV6,
core::num::NonZeroU8,
core::num::NonZeroU16,
core::num::NonZeroU32,
core::num::NonZeroU64,
core::num::NonZeroU128,
core::num::NonZeroUsize,
core::num::NonZeroI8,
core::num::NonZeroI16,
core::num::NonZeroI32,
core::num::NonZeroI64,
core::num::NonZeroI128,
core::num::NonZeroIsize,
#[cfg(feature = "uuid")]
uuid::Uuid,
#[cfg(feature = "jid")]
jid::Jid,
#[cfg(feature = "jid")]
jid::FullJid,
#[cfg(feature = "jid")]
jid::BareJid,
#[cfg(feature = "jid")]
jid::NodePart,
#[cfg(feature = "jid")]
jid::DomainPart,
#[cfg(feature = "jid")]
jid::ResourcePart,
#[cfg(feature = "serde_json")]
serde_json::Value,
}
impl FromXmlText for String {
/// Return the string unchanged.
fn from_xml_text(data: String) -> Result<Self, Error> {
Ok(data)
}
}
impl<T: FromXmlText, B: ToOwned<Owned = T>> FromXmlText for Cow<'_, B> {
/// Return a [`Cow::Owned`] containing the parsed value.
fn from_xml_text(data: String) -> Result<Self, Error> {
Ok(Cow::Owned(T::from_xml_text(data)?))
}
}
impl<T: FromXmlText> FromXmlText for Option<T> {
/// Return a [`Some`] containing the parsed value.
fn from_xml_text(data: String) -> Result<Self, Error> {
Ok(Some(T::from_xml_text(data)?))
}
}
impl<T: FromXmlText> FromXmlText for Box<T> {
/// Return a [`Box`] containing the parsed value.
fn from_xml_text(data: String) -> Result<Self, Error> {
Ok(Box::new(T::from_xml_text(data)?))
}
}
impl AsXmlText for String {
/// Return the borrowed string contents.
fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
Ok(Cow::Borrowed(self))
}
}
impl AsXmlText for str {
/// Return the borrowed string contents.
fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
Ok(Cow::Borrowed(self))
}
}
impl AsXmlText for &str {
/// Return the borrowed string contents.
fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
Ok(Cow::Borrowed(self))
}
}
impl<T: AsXmlText> AsXmlText for Box<T> {
/// Return the borrowed [`Box`] contents.
fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
T::as_xml_text(self)
}
}
impl<B: AsXmlText + ToOwned> AsXmlText for Cow<'_, B> {
/// Return the borrowed [`Cow`] contents.
fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
B::as_xml_text(self)
}
}
impl<T: AsXmlText> AsXmlText for &T {
/// Delegate to the `AsXmlText` implementation on `T`.
fn as_xml_text(&self) -> Result<Cow<'_, str>, Error> {
T::as_xml_text(*self)
}
}
impl<T: AsXmlText> AsOptionalXmlText for T {
fn as_optional_xml_text(&self) -> Result<Option<Cow<'_, str>>, Error> {
<Self as AsXmlText>::as_optional_xml_text(self)
}
}
impl<T: AsXmlText> AsOptionalXmlText for Option<T> {
fn as_optional_xml_text(&self) -> Result<Option<Cow<'_, str>>, Error> {
self.as_ref()
.map(T::as_optional_xml_text)
.transpose()
.map(Option::flatten)
}
}
/// Represent a way to encode/decode text data into a Rust type.
///
/// This trait can be used in scenarios where implementing [`FromXmlText`]
/// and/or [`AsXmlText`] on a type is not feasible or sensible, such as the
/// following:
///
/// 1. The type originates in a foreign crate, preventing the implementation
/// of foreign traits.
///
/// 2. There is more than one way to convert a value to/from XML.
///
/// The codec to use for a text can be specified in the attributes understood
/// by `FromXml` and `AsXml` derive macros. See the documentation of the
/// [`FromXml`][`macro@crate::FromXml`] derive macro for details.
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot be used as XML text codec for values of type `{T}`."
)]
pub trait TextCodec<T> {
/// Decode a string value into the type.
fn decode(&self, s: String) -> Result<T, Error>;
/// Encode the type as string value.
///
/// If this returns `None`, the string value is not emitted at all.
fn encode<'x>(&self, value: &'x T) -> Result<Option<Cow<'x, str>>, Error>;
/// Apply a filter to this codec.
///
/// Filters preprocess strings before they are handed to the codec for
/// parsing, allowing to, for example, make the codec ignore irrelevant
/// content by stripping it.
// NOTE: The bound on T is needed because any given type A may implement
// TextCodec for any number of types. If we pass T down to the `Filtered`
// struct, rustc can do type inference on which `TextCodec`
// implementation the `filtered` method is supposed to have been called
// on.
fn filtered<F: TextFilter>(self, filter: F) -> Filtered<F, Self, T>
where
// placing the bound here (instead of on the `TextCodec<T>` trait
// itself) preserves object-safety of TextCodec<T>.
Self: Sized,
{
Filtered {
filter,
codec: self,
bound: PhantomData,
}
}
}
/// Wrapper struct to apply a filter to a codec.
///
/// You can construct a value of this type via [`TextCodec::filtered`].
// NOTE: see the note on TextCodec::filtered for why we bind `T` here, too.
pub struct Filtered<F, C, T> {
filter: F,
codec: C,
bound: PhantomData<T>,
}
impl<T, F: TextFilter, C: TextCodec<T>> TextCodec<T> for Filtered<F, C, T> {
fn decode(&self, s: String) -> Result<T, Error> {
let s = self.filter.preprocess(s);
self.codec.decode(s)
}
fn encode<'x>(&self, value: &'x T) -> Result<Option<Cow<'x, str>>, Error> {
self.codec.encode(value)
}
}
/// Text codec which does no transform.
pub struct Plain;
impl TextCodec<String> for Plain {
fn decode(&self, s: String) -> Result<String, Error> {
Ok(s)
}
fn encode<'x>(&self, value: &'x String) -> Result<Option<Cow<'x, str>>, Error> {
Ok(Some(Cow::Borrowed(value.as_str())))
}
}
/// Text codec which returns `None` if the input to decode is the empty string, instead of
/// attempting to decode it.
///
/// Particularly useful when parsing `Option<T>` on `#[xml(text)]`, which does not support
/// `Option<_>` otherwise.
pub struct EmptyAsNone;
impl<T> TextCodec<Option<T>> for EmptyAsNone
where
T: FromXmlText + AsXmlText,
{
fn decode(&self, s: String) -> Result<Option<T>, Error> {
if s.is_empty() {
Ok(None)
} else {
Some(T::from_xml_text(s)).transpose()
}
}
fn encode<'x>(&self, value: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
Ok(value
.as_ref()
.map(AsXmlText::as_xml_text)
.transpose()?
.and_then(|v| (!v.is_empty()).then_some(v)))
}
}
/// Text codec which returns None instead of the empty string.
pub struct EmptyAsError;
impl TextCodec<String> for EmptyAsError {
fn decode(&self, s: String) -> Result<String, Error> {
if s.is_empty() {
Err(Error::Other("Empty text node."))
} else {
Ok(s)
}
}
fn encode<'x>(&self, value: &'x String) -> Result<Option<Cow<'x, str>>, Error> {
if value.is_empty() {
Err(Error::Other("Empty text node."))
} else {
Ok(Some(Cow::Borrowed(value.as_str())))
}
}
}
/// Trait for preprocessing text data from XML.
///
/// This may be used by codecs to allow to customize some of their behaviour.
pub trait TextFilter {
/// Process the incoming string and return the result of the processing.
fn preprocess(&self, s: String) -> String;
}
/// Text preprocessor which returns the input unchanged.
pub struct NoFilter;
impl TextFilter for NoFilter {
fn preprocess(&self, s: String) -> String {
s
}
}
/// Text preprocessor to remove all whitespace.
pub struct StripWhitespace;
impl TextFilter for StripWhitespace {
fn preprocess(&self, s: String) -> String {
let s: String = s
.chars()
.filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t')
.collect();
s
}
}
/// Text codec transforming text to binary using standard `base64`.
///
/// `Base64` uses the [`base64::engine::general_purpose::STANDARD`] engine.
/// [`TextCodec`] is also automatically implemented for any value which
/// implements [`base64::engine::Engine`], allowing you to choose different
/// alphabets easily.
#[cfg(feature = "base64")]
pub struct Base64;
#[cfg(feature = "base64")]
impl TextCodec<Vec<u8>> for Base64 {
fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
base64::engine::Engine::decode(&StandardBase64Engine, s.as_bytes())
.map_err(Error::text_parse_error)
}
fn encode<'x>(&self, value: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
Ok(Some(Cow::Owned(base64::engine::Engine::encode(
&StandardBase64Engine,
value,
))))
}
}
#[cfg(feature = "base64")]
impl<'x> TextCodec<Cow<'x, [u8]>> for Base64 {
fn decode(&self, s: String) -> Result<Cow<'x, [u8]>, Error> {
base64::engine::Engine::decode(&StandardBase64Engine, s.as_bytes())
.map_err(Error::text_parse_error)
.map(Cow::Owned)
}
fn encode<'a>(&self, value: &'a Cow<'x, [u8]>) -> Result<Option<Cow<'a, str>>, Error> {
Ok(Some(Cow::Owned(base64::engine::Engine::encode(
&StandardBase64Engine,
value,
))))
}
}
#[cfg(feature = "base64")]
impl<T> TextCodec<Option<T>> for Base64
where
Base64: TextCodec<T>,
{
fn decode(&self, s: String) -> Result<Option<T>, Error> {
if s.is_empty() {
return Ok(None);
}
Ok(Some(self.decode(s)?))
}
fn encode<'x>(&self, decoded: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
decoded
.as_ref()
.map(|x| self.encode(x))
.transpose()
.map(Option::flatten)
}
}
#[cfg(feature = "base64")]
impl<T: base64::engine::Engine> TextCodec<Vec<u8>> for T {
fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
base64::engine::Engine::decode(self, s.as_bytes()).map_err(Error::text_parse_error)
}
fn encode<'x>(&self, value: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
Ok(Some(Cow::Owned(base64::engine::Engine::encode(
self, value,
))))
}
}
#[cfg(feature = "base64")]
impl<T: base64::engine::Engine, U> TextCodec<Option<U>> for T
where
T: TextCodec<U>,
{
fn decode(&self, s: String) -> Result<Option<U>, Error> {
if s.is_empty() {
return Ok(None);
}
Ok(Some(TextCodec::decode(self, s)?))
}
fn encode<'x>(&self, decoded: &'x Option<U>) -> Result<Option<Cow<'x, str>>, Error> {
decoded
.as_ref()
.map(|x| TextCodec::encode(self, x))
.transpose()
.map(Option::flatten)
}
}
/// Text codec transforming text to binary using hexadecimal nibbles.
///
/// The length must be known at compile-time.
pub struct FixedHex<const N: usize>;
impl<const N: usize> TextCodec<[u8; N]> for FixedHex<N> {
fn decode(&self, s: String) -> Result<[u8; N], Error> {
if s.len() != 2 * N {
return Err(Error::Other("Invalid length"));
}
let mut bytes = [0u8; N];
for i in 0..N {
bytes[i] =
u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(Error::text_parse_error)?;
}
Ok(bytes)
}
fn encode<'x>(&self, value: &'x [u8; N]) -> Result<Option<Cow<'x, str>>, Error> {
let mut bytes = String::with_capacity(N * 2);
for byte in value {
bytes.extend(format!("{:02x}", byte).chars());
}
Ok(Some(Cow::Owned(bytes)))
}
}
impl<T, const N: usize> TextCodec<Option<T>> for FixedHex<N>
where
FixedHex<N>: TextCodec<T>,
{
fn decode(&self, s: String) -> Result<Option<T>, Error> {
if s.is_empty() {
return Ok(None);
}
Ok(Some(self.decode(s)?))
}
fn encode<'x>(&self, decoded: &'x Option<T>) -> Result<Option<Cow<'x, str>>, Error> {
decoded
.as_ref()
.map(|x| self.encode(x))
.transpose()
.map(Option::flatten)
}
}
/// Text codec for colon-separated bytes of uppercase hexadecimal.
pub struct ColonSeparatedHex;
impl TextCodec<Vec<u8>> for ColonSeparatedHex {
fn decode(&self, s: String) -> Result<Vec<u8>, Error> {
assert_eq!((s.len() + 1) % 3, 0);
let mut bytes = Vec::with_capacity((s.len() + 1) / 3);
for i in 0..(1 + s.len()) / 3 {
let byte =
u8::from_str_radix(&s[3 * i..3 * i + 2], 16).map_err(Error::text_parse_error)?;
if 3 * i + 2 < s.len() {
assert_eq!(&s[3 * i + 2..3 * i + 3], ":");
}
bytes.push(byte);
}
Ok(bytes)
}
fn encode<'x>(&self, decoded: &'x Vec<u8>) -> Result<Option<Cow<'x, str>>, Error> {
// TODO: Super inefficient!
let mut bytes = Vec::with_capacity(decoded.len());
for byte in decoded {
bytes.push(format!("{:02X}", byte));
}
Ok(Some(Cow::Owned(bytes.join(":"))))
}
}