xso: provide adapter for AsXml implementation based on Into<Element>

Jonas Schäfer created

Analogous to the already existing `IntoXml` implementation helpers
based on an `Into<Element>` implementation on a type, this provides
the utilities for `AsXml`.

This is of course exceptionally inefficient, but it is only needed
transitionally until we have everything migrated to derive macros or
otherwise rewritten in terms of AsXml/FromXml.

Change summary

xso/src/minidom_compat.rs |  31 +++++
xso/src/rxml_util.rs      | 217 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 247 insertions(+), 1 deletion(-)

Detailed changes

xso/src/minidom_compat.rs 🔗

@@ -19,7 +19,7 @@ use rxml::{
 
 use crate::{
     error::{Error, FromEventsError},
-    rxml_util::Item,
+    rxml_util::{EventToItem, Item},
     AsXml, FromEventsBuilder, FromXml, IntoXml,
 };
 
@@ -453,6 +453,35 @@ impl Iterator for IntoEventsViaElement {
     }
 }
 
+/// Helper struct to stream a struct which implements conversion
+/// to [`minidom::Element`].
+pub struct AsItemsViaElement<'x> {
+    iter: EventToItem<IntoEventsViaElement>,
+    lifetime_binding: PhantomData<Item<'x>>,
+}
+
+impl<'x> AsItemsViaElement<'x> {
+    /// Create a new streaming parser for `T`.
+    pub fn new<E, T>(value: T) -> Result<Self, crate::error::Error>
+    where
+        Error: From<E>,
+        minidom::Element: TryFrom<T, Error = E>,
+    {
+        Ok(Self {
+            iter: EventToItem::new(IntoEventsViaElement::new(value)?),
+            lifetime_binding: PhantomData,
+        })
+    }
+}
+
+impl<'x> Iterator for AsItemsViaElement<'x> {
+    type Item = Result<Item<'x>, Error>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.iter.next().map(|x| x.map(Item::into_owned))
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

xso/src/rxml_util.rs 🔗

@@ -9,6 +9,8 @@
 use std::borrow::Cow;
 
 use rxml::{Namespace, NcNameStr, XmlVersion};
+#[cfg(feature = "minidom")]
+use rxml::Event;
 
 /// An encodable item.
 ///
@@ -90,3 +92,218 @@ impl Item<'_> {
         }
     }
 }
+
+/// Iterator adapter which converts an iterator over [`Event`][`rxml::Event`]
+/// to an iterator over [`Item<'static>`][`Item`].
+///
+/// This iterator consumes the events and returns items which contain the data
+/// in an owned fashion.
+#[cfg(feature = "minidom")]
+pub(crate) struct EventToItem<I> {
+    inner: I,
+    attributes: Option<rxml::xml_map::IntoIter<String>>,
+}
+
+#[cfg(feature = "minidom")]
+impl<I> EventToItem<I> {
+    pub(crate) fn new(inner: I) -> Self {
+        Self {
+            inner,
+            attributes: None,
+        }
+    }
+
+    fn drain(&mut self) -> Option<Item<'static>> {
+        match self.attributes {
+            Some(ref mut attrs) => {
+                if let Some(((ns, name), value)) = attrs.next() {
+                    Some(Item::Attribute(ns, Cow::Owned(name), Cow::Owned(value)))
+                } else {
+                    self.attributes = None;
+                    Some(Item::ElementHeadEnd)
+                }
+            }
+            None => None,
+        }
+    }
+
+    fn update(&mut self, ev: Event) -> Item<'static> {
+        assert!(self.attributes.is_none());
+        match ev {
+            Event::XmlDeclaration(_, v) => Item::XmlDeclaration(v),
+            Event::StartElement(_, (ns, name), attrs) => {
+                self.attributes = Some(attrs.into_iter());
+                Item::ElementHeadStart(ns, Cow::Owned(name))
+            }
+            Event::Text(_, value) => Item::Text(Cow::Owned(value)),
+            Event::EndElement(_) => Item::ElementFoot,
+        }
+    }
+}
+
+#[cfg(feature = "minidom")]
+impl<I: Iterator<Item = Result<Event, crate::error::Error>>> Iterator for EventToItem<I> {
+    type Item = Result<Item<'static>, crate::error::Error>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(item) = self.drain() {
+            return Some(Ok(item));
+        }
+        let next = match self.inner.next() {
+            Some(Ok(v)) => v,
+            Some(Err(e)) => return Some(Err(e)),
+            None => return None,
+        };
+        Some(Ok(self.update(next)))
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        // we may create an indefinte amount of items for a single event,
+        // so we cannot provide a reasonable upper bound.
+        (self.inner.size_hint().0, None)
+    }
+}
+
+#[cfg(all(test, feature = "minidom"))]
+mod tests_minidom {
+    use std::convert::TryInto;
+
+    use rxml::{parser::EventMetrics, AttrMap};
+
+    use super::*;
+
+    fn events_to_items<I: Iterator<Item = Event>>(events: I) -> Vec<Item<'static>> {
+        let iter = EventToItem {
+            inner: events.map(|ev| Ok(ev)),
+            attributes: None,
+        };
+        let mut result = Vec::new();
+        for item in iter {
+            let item = item.unwrap();
+            result.push(item);
+        }
+        result
+    }
+
+    #[test]
+    fn event_to_item_xml_declaration() {
+        let events = vec![Event::XmlDeclaration(
+            EventMetrics::zero(),
+            XmlVersion::V1_0,
+        )];
+        let items = events_to_items(events.into_iter());
+        assert_eq!(items.len(), 1);
+        match items[0] {
+            Item::XmlDeclaration(XmlVersion::V1_0) => (),
+            ref other => panic!("unexected item in position 0: {:?}", other),
+        };
+    }
+
+    #[test]
+    fn event_to_item_empty_element() {
+        let events = vec![
+            Event::StartElement(
+                EventMetrics::zero(),
+                (Namespace::NONE, "elem".try_into().unwrap()),
+                AttrMap::new(),
+            ),
+            Event::EndElement(EventMetrics::zero()),
+        ];
+        let items = events_to_items(events.into_iter());
+        assert_eq!(items.len(), 3);
+        match items[0] {
+            Item::ElementHeadStart(ref ns, ref name) => {
+                assert_eq!(&**ns, Namespace::none());
+                assert_eq!(&**name, "elem");
+            }
+            ref other => panic!("unexected item in position 0: {:?}", other),
+        };
+        match items[1] {
+            Item::ElementHeadEnd => (),
+            ref other => panic!("unexected item in position 1: {:?}", other),
+        };
+        match items[2] {
+            Item::ElementFoot => (),
+            ref other => panic!("unexected item in position 2: {:?}", other),
+        };
+    }
+
+    #[test]
+    fn event_to_item_element_with_attributes() {
+        let mut attrs = AttrMap::new();
+        attrs.insert(
+            Namespace::NONE,
+            "attr".try_into().unwrap(),
+            "value".to_string(),
+        );
+        let events = vec![
+            Event::StartElement(
+                EventMetrics::zero(),
+                (Namespace::NONE, "elem".try_into().unwrap()),
+                attrs,
+            ),
+            Event::EndElement(EventMetrics::zero()),
+        ];
+        let items = events_to_items(events.into_iter());
+        assert_eq!(items.len(), 4);
+        match items[0] {
+            Item::ElementHeadStart(ref ns, ref name) => {
+                assert_eq!(&**ns, Namespace::none());
+                assert_eq!(&**name, "elem");
+            }
+            ref other => panic!("unexected item in position 0: {:?}", other),
+        };
+        match items[1] {
+            Item::Attribute(ref ns, ref name, ref value) => {
+                assert_eq!(&**ns, Namespace::none());
+                assert_eq!(&**name, "attr");
+                assert_eq!(&**value, "value");
+            }
+            ref other => panic!("unexected item in position 1: {:?}", other),
+        };
+        match items[2] {
+            Item::ElementHeadEnd => (),
+            ref other => panic!("unexected item in position 2: {:?}", other),
+        };
+        match items[3] {
+            Item::ElementFoot => (),
+            ref other => panic!("unexected item in position 3: {:?}", other),
+        };
+    }
+
+    #[test]
+    fn event_to_item_element_with_text() {
+        let events = vec![
+            Event::StartElement(
+                EventMetrics::zero(),
+                (Namespace::NONE, "elem".try_into().unwrap()),
+                AttrMap::new(),
+            ),
+            Event::Text(EventMetrics::zero(), "Hello World!".to_owned()),
+            Event::EndElement(EventMetrics::zero()),
+        ];
+        let items = events_to_items(events.into_iter());
+        assert_eq!(items.len(), 4);
+        match items[0] {
+            Item::ElementHeadStart(ref ns, ref name) => {
+                assert_eq!(&**ns, Namespace::none());
+                assert_eq!(&**name, "elem");
+            }
+            ref other => panic!("unexected item in position 0: {:?}", other),
+        };
+        match items[1] {
+            Item::ElementHeadEnd => (),
+            ref other => panic!("unexected item in position 1: {:?}", other),
+        };
+        match items[2] {
+            Item::Text(ref value) => {
+                assert_eq!(value, "Hello World!");
+            }
+            ref other => panic!("unexected item in position 2: {:?}", other),
+        };
+        match items[3] {
+            Item::ElementFoot => (),
+            ref other => panic!("unexected item in position 3: {:?}", other),
+        };
+    }
+}