xso: add support for dynamically typed XSOs

Jonas Schäfer created

Change summary

xso/ChangeLog     |   2 
xso/src/dynxso.rs | 886 +++++++++++++++++++++++++++++++++++++++++++++++++
xso/src/lib.rs    |   2 
3 files changed, 890 insertions(+)

Detailed changes

xso/ChangeLog 🔗

@@ -57,6 +57,8 @@ Version NEXT:
       - `FromXml::xml_name_matcher()` and related types, which allow `FromXml`
         implementors to provide cachable hints to callers about which XML
         elements are valid candidates for parsing (!573).
+      - `Xso<dyn Trait>`, a Box-like struct which can hold a dynamically-typed
+        XSO, alongside related traits and (declarative) macros (!573).
     * Changes
       - Generated AsXml iterator and FromXml builder types are now
         doc(hidden), to not clutter hand-written documentation with auto

xso/src/dynxso.rs 🔗

@@ -0,0 +1,886 @@
+// Copyright (c) 2025 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/.
+
+//! # Dynamically-typed XSOs
+//!
+//! This module provides the utilities to make dynamically-typed XSOs work.
+//! Dynamically typed XSOs are contained in [`Xso<dyn Trait>`][`Xso`], where
+//! `Trait` is a trait provided by the user.
+//!
+//! The given `Trait` constrains the specific types which can be used in the
+//! `Xso<dyn Trait>` box. This allows users to provide additional methods on
+//! the trait which are available on all `Xso<dyn Trait>` objects via the
+//! [`Deref`][`core::ops::Deref`] and [`DerefMut`][`core::ops::DerefMut`]
+//! implementations.
+//!
+//! ## Creating a new `Trait`
+//!
+//! In order to be usable within `Xso<dyn Trait>`, a trait must satisfy the
+//! following constraints:
+//!
+//! - `dyn Trait` must implement [`DynXso`].
+//! - `dyn Trait` must implement [`MayContain<T>`] for all `T` which implement
+//!   `Trait`.
+//!
+//! The easiest and most forward-compatible way of providing these
+//! implementations is the [`derive_dyn_traits`][`crate::derive_dyn_traits`]
+//! macro.
+//!
+//! ## 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 core::any::Any;
+//! # use xso::{dynxso::{Xso, BuilderRegistry}, FromXml, from_bytes, derive_dyn_traits};
+//! trait MyPayload: Any {}
+//!
+//! derive_dyn_traits!(MyPayload);
+//!
+//! #[derive(FromXml, Debug, PartialEq)]
+//! #[xml(namespace = "urn:example", name = "foo")]
+//! struct Foo;
+//! impl MyPayload for Foo {}
+//! Xso::<dyn MyPayload>::register_type::<Foo>();
+//!
+//! #[derive(FromXml, Debug, PartialEq)]
+//! #[xml(namespace = "urn:example", name = "bar")]
+//! struct Bar;
+//! impl MyPayload for Bar {}
+//! Xso::<dyn MyPayload>::register_type::<Bar>();
+//!
+//! let x: Xso<dyn MyPayload> = from_bytes("<foo xmlns='urn:example'/>".as_bytes()).unwrap();
+//! assert_eq!(Foo, *x.downcast::<Foo>().unwrap());
+//!
+//! let x: Xso<dyn MyPayload> = from_bytes("<bar xmlns='urn:example'/>".as_bytes()).unwrap();
+//! assert_eq!(Bar, *x.downcast::<Bar>().unwrap());
+//! ```
+
+use alloc::{boxed::Box, vec::Vec};
+use core::{
+    any::{Any, TypeId},
+    fmt,
+    marker::PhantomData,
+    ops::{Deref, DerefMut},
+};
+use std::sync::Mutex;
+
+use crate::{
+    asxml::AsXmlDyn,
+    error::{Error, FromEventsError},
+    fromxml::XmlNameMatcher,
+    AsXml, Context, FromEventsBuilder, FromXml, Item,
+};
+
+/// # Generate `DynXso` and `MayContain` trait implementations
+///
+/// This macro generates trait [`DynXso`] and [`MayContain`] trait
+/// implementations for a given trait. For more background information on when
+/// that is a useful thing to have, see the [`dynxso`][`crate::dynxso`]
+/// module.
+///
+/// ## Example
+///
+/// ```
+/// # use core::any::Any;
+/// use xso::derive_dyn_traits;
+/// trait Foo: Any {}
+/// derive_dyn_traits!(Foo);
+/// ```
+///
+/// Note that the trait this macro is called on **must** have a bound on
+/// `Any`, otherwise the generated code will not compile:
+///
+/// ```compile_fail
+/// use xso::derive_dyn_traits;
+/// trait Foo {}
+/// derive_dyn_traits!(Foo);
+/// // ↑ will generate a bunch of errors about incompatible types
+/// ```
+#[macro_export]
+macro_rules! derive_dyn_traits {
+    ($trait:ident) => {
+        impl $crate::dynxso::DynXso for dyn $trait {
+            fn registry() -> &'static $crate::dynxso::BuilderRegistry<Self> {
+                static DATA: $crate::dynxso::BuilderRegistry<dyn $trait> =
+                    $crate::dynxso::BuilderRegistry::new();
+                &DATA
+            }
+
+            fn try_downcast<T: 'static>(
+                self: $crate::exports::alloc::boxed::Box<Self>,
+            ) -> Result<
+                $crate::exports::alloc::boxed::Box<T>,
+                $crate::exports::alloc::boxed::Box<Self>,
+            >
+            where
+                Self: $crate::dynxso::MayContain<T>,
+            {
+                if (&*self as &dyn core::any::Any).is::<T>() {
+                    match (self as $crate::exports::alloc::boxed::Box<dyn core::any::Any>)
+                        .downcast()
+                    {
+                        Ok(v) => Ok(v),
+                        Err(_) => unreachable!("Any::is and Any::downcast disagree!"),
+                    }
+                } else {
+                    Err(self)
+                }
+            }
+
+            fn try_downcast_ref<T: 'static>(&self) -> Option<&T>
+            where
+                Self: $crate::dynxso::MayContain<T>,
+            {
+                (&*self as &dyn core::any::Any).downcast_ref()
+            }
+
+            fn try_downcast_mut<T: 'static>(&mut self) -> Option<&mut T>
+            where
+                Self: $crate::dynxso::MayContain<T>,
+            {
+                (&mut *self as &mut dyn core::any::Any).downcast_mut()
+            }
+
+            fn is<T: 'static>(&self) -> bool
+            where
+                Self: $crate::dynxso::MayContain<T>,
+            {
+                (&*self as &dyn core::any::Any).is::<T>()
+            }
+
+            fn type_id(&self) -> core::any::TypeId {
+                (&*self as &dyn core::any::Any).type_id()
+            }
+        }
+
+        impl<T: $trait> $crate::dynxso::MayContain<T> for dyn $trait {
+            fn upcast_into(other: T) -> Box<Self> {
+                Box::new(other)
+            }
+        }
+    };
+}
+
+type BuilderRegistryBuilder<T> = Box<
+    dyn Fn(
+            rxml::QName,
+            rxml::AttrMap,
+            &Context<'_>,
+        ) -> Result<Box<dyn FromEventsBuilder<Output = Box<T>>>, FromEventsError>
+        + Send
+        + Sync
+        + 'static,
+>;
+
+struct BuilderRegistryEntry<T: ?Sized> {
+    matcher: XmlNameMatcher<'static>,
+    ty: TypeId,
+    builder: BuilderRegistryBuilder<T>,
+}
+
+impl<T: ?Sized> fmt::Debug for BuilderRegistryEntry<T> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("BuilderRegistryEntry")
+            .field("matcher", &self.matcher)
+            .field("ty", &self.ty)
+            .finish_non_exhaustive()
+    }
+}
+
+/// # Registry for type-erased [`FromXml`] builders which construct `T`
+///
+/// This registry holds type-erased [`FromXml::from_events`] implementations.
+/// It can be used to dynamically dispatch to a set of `FromXml`
+/// implementations which is not known at compile time.
+///
+/// Under the hood, this is used by the `FromXml` implementation on
+/// [`Xso<T>`][`Xso`], via the [`DynXso`] trait.
+///
+/// Note that this registry does not allow to add arbitrary builders. All
+/// builders must originate in a [`FromXml`] implementation and their output
+/// must be convertible to `T`.
+///
+/// ## Performance trade-offs
+///
+/// The implementation of the `BuilderRegistry` is optimized toward specific
+/// usages. Other usages may see negative performance impacts. The following
+/// assumptions are made:
+///
+/// - Types are added only once at startup and a matching
+///   [`reserve`][`Self::reserve`] call is made beforehands.
+/// - There are many different types.
+/// - [`FromXml::xml_name_matcher`] returns a different value for most of the
+///   types which are added.
+///
+/// The lookup algorithms in particular are geared toward the implications of
+/// the last two assumptions and may be rather inefficient otherwise.
+///
+/// ## Example
+///
+/// This example illustrates the manual usage of the `BuilderRegistry`.
+///
+#[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::{dynxso::{BuilderRegistry, MayContain}, FromXml, from_bytes, exports::rxml::{Namespace, AttrMap, QName, Event, parser::EventMetrics, xml_ncname}, Context, FromEventsBuilder, error::FromEventsError};
+/// #[derive(FromXml, Debug, PartialEq)]
+/// #[xml(namespace = "urn:example", name = "foo")]
+/// struct Foo;
+///
+/// #[derive(FromXml, Debug, PartialEq)]
+/// #[xml(namespace = "urn:example", name = "bar")]
+/// struct Bar;
+///
+/// #[derive(Debug, PartialEq)]
+/// enum Either {
+///    A(Foo),
+///    B(Bar),
+/// }
+///
+/// // BuilderRegistry::add::<U> requires that Either: MayContain<U>, in order
+/// // to be able to wrap the U::Builder such that it outputs a Xso<Either>
+/// // instead.
+/// impl MayContain<Foo> for Either {
+///     fn upcast_into(other: Foo) -> Box<Self> { Box::new(Self::A(other)) }
+/// }
+///
+/// impl MayContain<Bar> for Either {
+///     fn upcast_into(other: Bar) -> Box<Self> { Box::new(Self::B(other)) }
+/// }
+///
+/// let registry = BuilderRegistry::<Either>::new();
+/// registry.add::<Foo>();
+/// registry.add::<Bar>();
+///
+/// let mut builder = registry.make_builder(
+///     (
+///         Namespace::from_str("urn:example"),
+///         xml_ncname!("foo").to_owned(),   // <- Selects the Foo variant
+///     ),
+///     AttrMap::new(),
+///     &Context::empty(),
+/// ).unwrap();
+/// let x = builder.feed(Event::EndElement(EventMetrics::zero()), &Context::empty()).unwrap().unwrap();
+/// assert_eq!(Either::A(Foo), *x);
+///
+/// let mut builder = registry.make_builder(
+///     (
+///         Namespace::from_str("urn:example"),
+///         xml_ncname!("bar").to_owned(),   // <- Selects the Bar variant
+///     ),
+///     AttrMap::new(),
+///     &Context::empty(),
+/// ).unwrap();
+/// let x = builder.feed(Event::EndElement(EventMetrics::zero()), &Context::empty()).unwrap().unwrap();
+/// assert_eq!(Either::B(Bar), *x);
+/// ```
+///
+/// Implementing `FromXml` on the `Either` type in the above example would
+/// be trivial and look like this:
+///
+#[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::{dynxso::{BuilderRegistry, MayContain}, FromXml, from_bytes, exports::rxml::{Namespace, AttrMap, QName, Event, parser::EventMetrics}, Context, FromEventsBuilder, error::FromEventsError};
+/// # #[derive(FromXml, Debug, PartialEq)]
+/// # #[xml(namespace = "urn:example", name = "foo")]
+/// # struct Foo;
+/// #
+/// # #[derive(FromXml, Debug, PartialEq)]
+/// # #[xml(namespace = "urn:example", name = "bar")]
+/// # struct Bar;
+/// #
+/// # #[derive(Debug, PartialEq)]
+/// # enum Either {
+/// #    A(Foo),
+/// #    B(Bar),
+/// # }
+/// #
+/// # impl MayContain<Foo> for Either {
+/// #     fn upcast_into(other: Foo) -> Box<Self> { Box::new(Self::A(other)) }
+/// # }
+/// #
+/// # impl MayContain<Bar> for Either {
+/// #     fn upcast_into(other: Bar) -> Box<Self> { Box::new(Self::B(other)) }
+/// # }
+/// #
+/// // ... code from the previous example ...
+/// use xso::dynxso::UnboxBuilder;
+/// // In order to be able to use the registry from the FromXml implementation,
+/// // it must be a static (as opposed to a local variable as in the previous
+/// // example).
+/// static REGISTRY: BuilderRegistry<Either> = BuilderRegistry::new();
+/// REGISTRY.add::<Foo>();
+/// REGISTRY.add::<Bar>();
+///
+/// impl FromXml for Either {
+///     type Builder = UnboxBuilder<Box<dyn FromEventsBuilder<Output = Box<Either>>>>;
+///
+///     fn from_events(name: QName, attrs: AttrMap, ctx: &Context<'_>) -> Result<Self::Builder, FromEventsError> {
+///         REGISTRY.make_builder(name, attrs, ctx).map(UnboxBuilder::wrap)
+///     }
+/// }
+///
+/// assert_eq!(
+///     Either::A(Foo),
+///     from_bytes("<foo xmlns='urn:example'/>".as_bytes()).unwrap(),
+/// );
+///
+/// assert_eq!(
+///     Either::B(Bar),
+///     from_bytes("<bar xmlns='urn:example'/>".as_bytes()).unwrap(),
+/// );
+/// ```
+#[derive(Debug)]
+pub struct BuilderRegistry<T: ?Sized> {
+    inner: Mutex<Vec<BuilderRegistryEntry<T>>>,
+}
+
+impl<T: ?Sized + 'static> BuilderRegistry<T> {
+    /// Create an empty registry.
+    pub const fn new() -> Self {
+        Self {
+            inner: Mutex::new(Vec::new()),
+        }
+    }
+
+    fn insert(
+        &self,
+        matcher: XmlNameMatcher<'static>,
+        type_id: TypeId,
+        builder: BuilderRegistryBuilder<T>,
+    ) {
+        let mut registry = self.inner.lock().unwrap();
+        let start_scan_at = registry.partition_point(|entry| entry.matcher < matcher);
+        let insert_at = 'outer: {
+            let mut i = start_scan_at;
+            while i < registry.len() {
+                let entry = &registry[i];
+                if entry.matcher == matcher {
+                    if entry.ty == type_id {
+                        // Already inserted.
+                        return;
+                    }
+                    // Still entries with the same matcher -> continue.
+                    i += 1;
+                    continue;
+                }
+
+                // Found insertion point;
+                break 'outer i;
+            }
+
+            // The entire (rest of the) registry contains items matching
+            // `matcher`, but none matching the given `type_id` -> insert at
+            // the end.
+            registry.len()
+        };
+
+        registry.insert(
+            insert_at,
+            BuilderRegistryEntry {
+                matcher,
+                ty: type_id,
+                builder,
+            },
+        );
+    }
+
+    /// Reserve space for at least `n` additional types.
+    pub fn reserve(&self, n: usize) {
+        self.inner.lock().unwrap().reserve(n);
+    }
+
+    /// Add a new builder to the registry.
+    ///
+    /// This allows to add any `FromXml` implementation whose output can be
+    /// converted to `T`.
+    pub fn add<U: Any + FromXml>(&self)
+    where
+        T: MayContain<U>,
+    {
+        struct Wrapper<B, X: ?Sized> {
+            inner: B,
+            output: PhantomData<X>,
+        }
+
+        impl<X: ?Sized, O, B: FromEventsBuilder<Output = O>> FromEventsBuilder for Wrapper<B, X>
+        where
+            X: MayContain<O>,
+        {
+            type Output = Box<X>;
+
+            fn feed(
+                &mut self,
+                ev: rxml::Event,
+                ctx: &Context<'_>,
+            ) -> Result<Option<Self::Output>, Error> {
+                self.inner
+                    .feed(ev, ctx)
+                    .map(|x| x.map(|x| <X as MayContain<O>>::upcast_into(x)))
+            }
+        }
+
+        self.insert(
+            U::xml_name_matcher(),
+            TypeId::of::<U>(),
+            Box::new(|name, attrs, ctx| {
+                U::from_events(name, attrs, ctx).map(|builder| {
+                    Box::new(Wrapper {
+                        inner: builder,
+                        output: PhantomData,
+                    }) as Box<dyn FromEventsBuilder<Output = Box<T>>>
+                })
+            }),
+        )
+    }
+
+    fn try_build(
+        inner: &mut Vec<BuilderRegistryEntry<T>>,
+        mut name: rxml::QName,
+        mut attrs: rxml::AttrMap,
+        ctx: &Context<'_>,
+        matcher_builder: impl for<'x> FnOnce(&'x rxml::QName) -> XmlNameMatcher<'x>,
+    ) -> Result<Box<dyn FromEventsBuilder<Output = Box<T>>>, FromEventsError> {
+        let matcher = matcher_builder(&name);
+        let start_scan_at = inner.partition_point(|entry| entry.matcher < matcher);
+
+        for entry in &inner[start_scan_at..] {
+            if !entry.matcher.matches(&name) {
+                return Err(FromEventsError::Mismatch { name, attrs });
+            }
+
+            match (entry.builder)(name, attrs, ctx) {
+                Ok(v) => return Ok(v),
+                Err(FromEventsError::Invalid(e)) => return Err(FromEventsError::Invalid(e)),
+                Err(FromEventsError::Mismatch {
+                    name: new_name,
+                    attrs: new_attrs,
+                }) => {
+                    name = new_name;
+                    attrs = new_attrs;
+                }
+            }
+        }
+
+        Err(FromEventsError::Mismatch { name, attrs })
+    }
+
+    /// Make a builder for the given element header.
+    ///
+    /// This tries all applicable `FromXml` implementations which have
+    /// previously been added via [`add`][`Self::add`] in unspecified order.
+    /// The first implementation to either fail or succeed at constructing a
+    /// builder determines the result. Implementations which return a
+    /// [`FromEventsError::Mismatch`][`crate::error::FromEventsError::Mismatch`]
+    /// are ignored.
+    ///
+    /// If all applicable implementations return `Mismatch`, this function
+    /// returns `Mismatch`, too.
+    pub fn make_builder(
+        &self,
+        name: rxml::QName,
+        attrs: rxml::AttrMap,
+        ctx: &Context<'_>,
+    ) -> Result<Box<dyn FromEventsBuilder<Output = Box<T>>>, FromEventsError> {
+        let mut inner = self.inner.lock().unwrap();
+        let (name, attrs) = match Self::try_build(&mut *inner, name, attrs, ctx, |qname| {
+            XmlNameMatcher::Specific(qname.0.as_str(), qname.1.as_str())
+        }) {
+            Ok(v) => return Ok(v),
+            Err(FromEventsError::Invalid(e)) => return Err(FromEventsError::Invalid(e)),
+            Err(FromEventsError::Mismatch { name, attrs }) => (name, attrs),
+        };
+
+        let (name, attrs) = match Self::try_build(&mut *inner, name, attrs, ctx, |qname| {
+            XmlNameMatcher::InNamespace(qname.0.as_str())
+        }) {
+            Ok(v) => return Ok(v),
+            Err(FromEventsError::Invalid(e)) => return Err(FromEventsError::Invalid(e)),
+            Err(FromEventsError::Mismatch { name, attrs }) => (name, attrs),
+        };
+
+        Self::try_build(&mut *inner, name, attrs, ctx, |_| XmlNameMatcher::Any)
+    }
+}
+
+/// # Dynamic XSO type
+///
+/// This trait provides the infrastructure for dynamic XSO types. In
+/// particular, it provides:
+///
+/// - Access to a [`BuilderRegistry`] which allows constructing an instance of
+///   the dynamic XSO type from XML.
+/// - Downcasts to specific types.
+///
+/// Like [`MayContain`], it is typically implemented on `dyn Trait` for some
+/// `Trait` and it is best generated using
+/// [`derive_dyn_traits`][`crate::derive_dyn_traits`].
+///
+/// This trait explicitly provides the methods provided by [`Any`]. The reason
+/// for this duplication is that with `DynXso` being intended to be
+/// implemented on `dyn Trait`, code using this trait cannot cast the value
+/// to `dyn Any` to access the `downcast`-related methods (`type_id` would,
+/// in fact, work if `DynXso` had a bound on `Any`, but not the downcasts).
+///
+/// *Hint*: It should not be necessary for user code to directly interact
+/// with this trait.
+pub trait DynXso: 'static {
+    /// Return the builder registry for this dynamic type.
+    ///
+    /// The builder registry is used by [`Xso::register_type`] and the
+    /// `FromXml` implementation.
+    fn registry() -> &'static BuilderRegistry<Self>;
+
+    /// Try to downcast a boxed dynamic XSO to a specific type.
+    ///
+    /// If `self` contains a `T` (and thus, the downcast succeeds), `Ok(_)`
+    /// is returned. Otherwise, `Err(self)` is returned, allowing to chain
+    /// this function with other downcast attempts.
+    ///
+    /// This is similar to `downcast` on [`dyn Any`][`core::any::Any`].
+    fn try_downcast<T: 'static>(self: Box<Self>) -> Result<Box<T>, Box<Self>>
+    where
+        Self: MayContain<T>;
+
+    /// Try to downcast a dynamic XSO to a reference to a specific type.
+    ///
+    /// If `self` contains a `T` (and thus, the downcast succeeds), `Some(_)`
+    /// is returned. Otherwise, `None`.
+    ///
+    /// This is similar to `downcast_ref` on [`dyn Any`][`core::any::Any`].
+    fn try_downcast_ref<T: 'static>(&self) -> Option<&T>
+    where
+        Self: MayContain<T>;
+
+    /// Try to downcast a dynamic XSO to a mutable reference to a specific
+    /// type.
+    ///
+    /// If `self` contains a `T` (and thus, the downcast succeeds), `Some(_)`
+    /// is returned. Otherwise, `None`.
+    ///
+    /// This is similar to `downcast_mut` on [`dyn Any`][`core::any::Any`].
+    fn try_downcast_mut<T: 'static>(&mut self) -> Option<&mut T>
+    where
+        Self: MayContain<T>;
+
+    /// Return true if `self` contains a `T`.
+    ///
+    /// This is similar to `is` on [`dyn Any`][`core::any::Any`].
+    fn is<T: 'static>(&self) -> bool
+    where
+        Self: MayContain<T>;
+
+    /// Return the [`TypeId`] of `self`.
+    ///
+    /// This is similar to `type_id` on [`dyn Any`][`core::any::Any`].
+    fn type_id(&self) -> TypeId;
+}
+
+/// # Declare that `T` may be held by `Box<Self>`
+///
+/// This trait is used to constrain which types can be put in
+/// [`Xso<Self>`][`Xso`]. It is typically implemented on `dyn Trait` for all
+/// `T: Trait`.
+///
+/// To automatically generate suitable implementations of this trait, see
+/// [`derive_dyn_traits`][`crate::derive_dyn_traits`].
+///
+/// Implementation-wise, this trait is very similar to `Box<Self>: From<T>`.
+/// However, `From` is also used in many different circumstances and it cannot
+/// be suitably overloaded on `Box<_>`, so a new trait was introduced for this
+/// particular purpose.
+pub trait MayContain<T> {
+    /// Convert a value of `T` into `Box<Self>`.
+    fn upcast_into(other: T) -> Box<Self>;
+}
+
+/// # Dynamic XSO container
+///
+/// This container is very similar to `Box<_>`, but geared specifically toward
+/// the use with `T` being a `dyn Trait`. It also implements [`FromXml`]
+/// (unconditionally) and [`AsXml`] if `T` implements [`AsXmlDyn`].
+///
+/// In order to provide these features, `T` must implement [`DynXso`] and
+/// [`MayContain`]. Implementations for these traits can be generated using
+/// [`derive_dyn_traits`][`crate::derive_dyn_traits`].
+///
+/// Most methods on `Xso<dyn Trait>` which take type parameters are only
+/// available for types `U` implementing `Trait` (or, more precisely, where
+/// `dyn Trait` implements `MayContain<U>`).
+#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
+#[repr(transparent)]
+pub struct Xso<T: ?Sized> {
+    inner: Box<T>,
+}
+
+impl<T: ?Sized> Deref for Xso<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        self.inner.deref()
+    }
+}
+
+impl<T: ?Sized> DerefMut for Xso<T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.inner.deref_mut()
+    }
+}
+
+impl<T: DynXso + ?Sized> fmt::Debug for Xso<T> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("Xso")
+            .field("inner", &self.inner_type_id())
+            .finish()
+    }
+}
+
+impl<T: ?Sized> Xso<T> {
+    /// Wrap a value into a `Xso<dyn Trait>`.
+    ///
+    /// ```
+    /// # use core::any::Any;
+    /// # use xso::{dynxso::Xso, derive_dyn_traits};
+    /// trait Trait: Any {}
+    /// derive_dyn_traits!(Trait);
+    ///
+    /// struct Foo;
+    /// impl Trait for Foo {}
+    ///
+    /// let x: Xso<dyn Trait> = Xso::wrap(Foo);
+    /// ```
+    pub fn wrap<U: 'static>(value: U) -> Self
+    where
+        T: MayContain<U>,
+    {
+        Self {
+            inner: T::upcast_into(value),
+        }
+    }
+
+    /// Convert `Xso<T>` into `Box<T>`.
+    ///
+    /// ```
+    /// # use core::any::Any;
+    /// # use xso::{dynxso::Xso, derive_dyn_traits};
+    /// trait Trait: Any {}
+    /// derive_dyn_traits!(Trait);
+    ///
+    /// struct Foo;
+    /// impl Trait for Foo {}
+    ///
+    /// let x: Xso<dyn Trait> = Xso::wrap(Foo);
+    /// let x: Box<dyn Trait> = x.into_boxed();
+    /// ```
+    pub fn into_boxed(self) -> Box<T> {
+        self.inner
+    }
+}
+
+impl<T: DynXso + ?Sized + 'static> Xso<T> {
+    /// Downcast `self` to `Box<U>`.
+    ///
+    /// If the downcast fails, `self` is returned without change.
+    ///
+    /// ```
+    /// # use core::any::Any;
+    /// # use xso::{dynxso::Xso, derive_dyn_traits};
+    /// trait Trait: Any {}
+    /// derive_dyn_traits!(Trait);
+    ///
+    /// struct Foo;
+    /// impl Trait for Foo {}
+    ///
+    /// struct Bar;
+    /// impl Trait for Bar {}
+    ///
+    /// let x: Xso<dyn Trait> = Xso::wrap(Foo);
+    /// // Does not contain a Bar, so downcast fails.
+    /// let x: Xso<dyn Trait> = x.downcast::<Bar>().err().unwrap();
+    /// // *Does* contain a Foo, so downcast succeeds.
+    /// let f: Foo = *x.downcast().unwrap();
+    /// ```
+    pub fn downcast<U: 'static>(self) -> Result<Box<U>, Self>
+    where
+        T: MayContain<U>,
+    {
+        match self.inner.try_downcast() {
+            Ok(v) => Ok(v),
+            Err(inner) => Err(Self { inner }),
+        }
+    }
+
+    /// Downcast `&self` to `&U`.
+    ///
+    /// ```
+    /// # use core::any::Any;
+    /// # use xso::{dynxso::Xso, derive_dyn_traits};
+    /// trait Trait: Any {}
+    /// derive_dyn_traits!(Trait);
+    ///
+    /// struct Foo;
+    /// impl Trait for Foo {}
+    ///
+    /// struct Bar;
+    /// impl Trait for Bar {}
+    ///
+    /// let x: Xso<dyn Trait> = Xso::wrap(Foo);
+    /// // Does not contain a Bar, so downcast fails.
+    /// assert!(x.downcast_ref::<Bar>().is_none());
+    /// // *Does* contain a Foo, so downcast succeeds.
+    /// let f: &Foo = x.downcast_ref().unwrap();
+    /// ```
+    pub fn downcast_ref<U: 'static>(&self) -> Option<&U>
+    where
+        T: MayContain<U>,
+    {
+        self.inner.try_downcast_ref()
+    }
+
+    /// Downcast `&mut self` to `&mut U`.
+    ///
+    /// ```
+    /// # use core::any::Any;
+    /// # use xso::{dynxso::Xso, derive_dyn_traits};
+    /// trait Trait: Any {}
+    /// derive_dyn_traits!(Trait);
+    ///
+    /// struct Foo;
+    /// impl Trait for Foo {}
+    ///
+    /// struct Bar;
+    /// impl Trait for Bar {}
+    ///
+    /// let mut x: Xso<dyn Trait> = Xso::wrap(Foo);
+    /// // Does not contain a Bar, so downcast fails.
+    /// assert!(x.downcast_mut::<Bar>().is_none());
+    /// // *Does* contain a Foo, so downcast succeeds.
+    /// let f: &mut Foo = x.downcast_mut().unwrap();
+    /// ```
+    pub fn downcast_mut<U: 'static>(&mut self) -> Option<&mut U>
+    where
+        T: MayContain<U>,
+    {
+        self.inner.try_downcast_mut()
+    }
+
+    fn inner_type_id(&self) -> TypeId {
+        (&self.inner as &dyn Any).type_id()
+    }
+
+    /// Register a new type to be constructible.
+    ///
+    /// Only types registered through this function can be parsed from XML via
+    /// the [`FromXml`] implementation on `Xso<T>`. See
+    /// [`dynxso`][`crate::dynxso`] for details.
+    ///
+    #[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 core::any::Any;
+    /// # use xso::{dynxso::Xso, derive_dyn_traits, from_bytes, FromXml};
+    /// trait Trait: Any {}
+    /// derive_dyn_traits!(Trait);
+    ///
+    /// #[derive(FromXml, PartialEq, Debug)]
+    /// #[xml(namespace = "urn:example", name = "foo")]
+    /// struct Foo;
+    /// impl Trait for Foo {}
+    ///
+    /// // Parsing fails, because register_type() has not been called for
+    /// // Foo:
+    /// assert!(from_bytes::<Xso<dyn Trait>>("<foo xmlns='urn:example'/>".as_bytes()).is_err());
+    ///
+    /// Xso::<dyn Trait>::register_type::<Foo>();
+    /// // After registering Foo with Xso<dyn Trait>, parsing succeeds and
+    /// // we can downcast to Foo:
+    /// let x: Xso<dyn Trait> = from_bytes("<foo xmlns='urn:example'/>".as_bytes()).unwrap();
+    /// assert_eq!(Foo, *x.downcast().unwrap());
+    /// ```
+    pub fn register_type<U: FromXml + 'static>()
+    where
+        T: MayContain<U>,
+    {
+        T::registry().add::<U>()
+    }
+}
+
+/// Wrapper around a `FromEventsBuilder` to convert a `Box<T>` output to a
+/// `Xso<T>` output.
+///
+/// Not constructible by users, only for internal use.
+pub struct DynBuilder<B> {
+    inner: B,
+}
+
+impl<T: DynXso + ?Sized + 'static, B: FromEventsBuilder<Output = Box<T>>> FromEventsBuilder
+    for DynBuilder<B>
+{
+    type Output = Xso<T>;
+
+    fn feed(&mut self, ev: rxml::Event, ctx: &Context) -> Result<Option<Self::Output>, Error> {
+        self.inner
+            .feed(ev, ctx)
+            .map(|x| x.map(|inner| Xso { inner }))
+    }
+}
+
+/// Wrapper around a `FromEventsBuilder` to convert a `Box<T>` output to a
+/// `T` output.
+pub struct UnboxBuilder<T> {
+    inner: T,
+}
+
+impl<O, T: FromEventsBuilder<Output = Box<O>>> UnboxBuilder<T> {
+    /// Wrap a `FromEventsBuilder` which generates `Box<O>`.
+    pub fn wrap(inner: T) -> Self {
+        Self { inner }
+    }
+}
+
+impl<O, T: FromEventsBuilder<Output = Box<O>>> FromEventsBuilder for UnboxBuilder<T> {
+    type Output = O;
+
+    fn feed(&mut self, ev: rxml::Event, ctx: &Context) -> Result<Option<Self::Output>, Error> {
+        self.inner.feed(ev, ctx).map(|x| x.map(|inner| *inner))
+    }
+}
+
+impl<T: DynXso + ?Sized + 'static> FromXml for Xso<T> {
+    type Builder = DynBuilder<Box<dyn FromEventsBuilder<Output = Box<T>>>>;
+
+    fn from_events(
+        name: rxml::QName,
+        attrs: rxml::AttrMap,
+        ctx: &Context<'_>,
+    ) -> Result<Self::Builder, FromEventsError> {
+        T::registry()
+            .make_builder(name, attrs, ctx)
+            .map(|inner| DynBuilder { inner })
+    }
+}
+
+impl<T: DynXso + AsXmlDyn + ?Sized + 'static> AsXml for Xso<T> {
+    type ItemIter<'x> = Box<dyn Iterator<Item = Result<Item<'x>, Error>> + 'x>;
+
+    fn as_xml_iter(&self) -> Result<Self::ItemIter<'_>, Error> {
+        self.inner.as_xml_dyn_iter()
+    }
+
+    fn as_xml_dyn_iter(&self) -> Result<Self::ItemIter<'_>, Error> {
+        self.inner.as_xml_dyn_iter()
+    }
+}

xso/src/lib.rs 🔗

@@ -31,6 +31,8 @@ extern crate std;
 use std::io;
 
 pub mod asxml;
+#[cfg(feature = "std")]
+pub mod dynxso;
 pub mod error;
 pub mod fromxml;
 #[cfg(feature = "minidom")]