xso: create library for streamed XML parsing

This library provides the traits to parse structs from XML and
serialise them into XML without having to buffer the document object
model in memory.

The only implementations it provides are for minidom, basically
providing a lower-level interface to `minidom::Element::from_reader` and
`minidom::Element::to_writer`.

This is the first stepping stone into a world where `xmpp_parsers` can
parse the structs directly from XML.
This commit is contained in:
Jonas Schäfer 2024-06-16 10:14:13 +02:00
commit 14a1d66bf8
6 changed files with 497 additions and 0 deletions

248
xso/src/minidom_compat.rs Normal file
View file

@ -0,0 +1,248 @@
//! Implementations of traits from this crate for minidom types
// 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/.
use std::vec::IntoIter;
use minidom::{Element, Node};
use rxml::{
parser::EventMetrics,
writer::{SimpleNamespaces, TrackNamespace},
AttrMap, Event, Name, Namespace, NcName,
};
use crate::{
error::{Error, FromEventsError},
FromEventsBuilder, FromXml, IntoXml,
};
/// State machine for converting a minidom Element into rxml events.
enum IntoEventsInner {
/// Element header: the element is still intact and we need to generate
/// the [`rxml::Event::StartElement`] event from the namespace, name, and
/// attributes.
Header(Element),
/// Content: The contents of the element are streamed as events.
Nodes {
/// Remaining child nodes (text and/or children) to emit.
remaining: IntoIter<Node>,
/// When emitting a child element, this is a nested [`IntoEvents`]
/// instance for that child element.
nested: Option<Box<IntoEvents>>,
},
/// End of iteration: this state generates an end-of-iterator state.
///
/// Note that the [`rxml::Event::EndElement`] event for the element itself
/// is generated by the iterator alraedy in the `Nodes` state, when
/// `nested` is None and `remaining` returns `None` from its `next()`
/// implementation.
Fin,
}
/// Create the parts for a [`rxml::Event::StartElement`] from a
/// [`minidom::Element`].
///
/// Note that this copies the attribute data as well as namespace and name.
/// This is due to limitations in the [`minidom::Element`] API.
// NOTE to developers: The limitations are not fully trivial to overcome:
// the attributes use a BTreeMap internally, which does not offer a `drain`
// iterator.
fn make_start_ev_parts(el: &Element) -> Result<(rxml::QName, AttrMap), Error> {
let name = NcName::try_from(el.name())?;
let namespace = Namespace::from(el.ns());
let mut attrs = AttrMap::new();
for (name, value) in el.attrs() {
let name = Name::try_from(name)?;
let (prefix, name) = name.split_name()?;
let namespace = if let Some(prefix) = prefix {
if prefix == "xml" {
Namespace::XML
} else {
let ns = match el.prefixes.get(&Some(prefix.into())) {
Some(v) => v,
None => {
panic!("undeclared xml namespace prefix in minidom::Element")
}
};
Namespace::from(ns.to_owned())
}
} else {
Namespace::NONE
};
attrs.insert(namespace, name, value.to_owned());
}
Ok(((namespace, name), attrs))
}
impl IntoEventsInner {
fn next(&mut self) -> Result<Option<Event>, Error> {
match self {
IntoEventsInner::Header(ref mut el) => {
let (qname, attrs) = make_start_ev_parts(el)?;
let event = Event::StartElement(EventMetrics::zero(), qname, attrs);
*self = IntoEventsInner::Nodes {
remaining: el.take_nodes().into_iter(),
nested: None,
};
return Ok(Some(event));
}
IntoEventsInner::Nodes {
ref mut nested,
ref mut remaining,
} => {
loop {
if let Some(nested) = nested.as_mut() {
if let Some(ev) = nested.next() {
return Some(ev).transpose();
}
}
match remaining.next() {
Some(Node::Text(text)) => {
return Ok(Some(Event::Text(EventMetrics::zero(), text)));
}
Some(Node::Element(el)) => {
*nested = Some(Box::new(el.into_event_iter()?));
// fallthrough to next loop iteration
}
None => {
// end of element, switch state and emit EndElement
*self = IntoEventsInner::Fin;
return Ok(Some(Event::EndElement(EventMetrics::zero())));
}
}
}
}
IntoEventsInner::Fin => Ok(None),
}
}
}
/// Convert a [`minidom::Element`] into [`rxml::Event`]s.
///
/// This can be constructed from the
/// [`IntoXml::into_event_iter`][`crate::IntoXml::into_event_iter`]
/// implementation on [`minidom::Element`].
pub struct IntoEvents(IntoEventsInner);
impl Iterator for IntoEvents {
type Item = Result<Event, Error>;
fn next(&mut self) -> Option<Self::Item> {
self.0.next().transpose()
}
}
impl IntoXml for Element {
type EventIter = IntoEvents;
fn into_event_iter(self) -> Result<Self::EventIter, Error> {
Ok(IntoEvents(IntoEventsInner::Header(self)))
}
}
/// Construct a [`minidom::Element`] from [`rxml::Event`]s
///
/// This can be constructed from the
/// [`FromXml::from_events`][`crate::FromXml::from_events`]
/// implementation on [`minidom::Element`].
pub struct ElementFromEvents {
inner: Option<Element>,
nested: Option<Box<ElementFromEvents>>,
}
impl FromEventsBuilder for ElementFromEvents {
type Output = minidom::Element;
fn feed(&mut self, ev: Event) -> Result<Option<Self::Output>, Error> {
let inner = self
.inner
.as_mut()
.expect("feed() called after it finished");
if let Some(nested) = self.nested.as_mut() {
match nested.feed(ev)? {
Some(v) => {
inner.append_child(v);
self.nested = None;
return Ok(None);
}
None => return Ok(None),
}
}
match ev {
Event::XmlDeclaration(_, _) => Ok(None),
Event::StartElement(_, qname, attrs) => {
let nested = match Element::from_events(qname, attrs) {
Ok(v) => v,
Err(FromEventsError::Invalid(e)) => return Err(e),
Err(FromEventsError::Mismatch { .. }) => {
unreachable!("<Element as FromXml>::from_events should accept everything!")
}
};
self.nested = Some(Box::new(nested));
Ok(None)
}
Event::Text(_, text) => {
inner.append_text_node(text);
Ok(None)
}
Event::EndElement(_) => Ok(Some(self.inner.take().unwrap())),
}
}
}
impl FromXml for Element {
type Builder = ElementFromEvents;
fn from_events(
qname: rxml::QName,
attrs: rxml::AttrMap,
) -> Result<Self::Builder, FromEventsError> {
let mut prefixes = SimpleNamespaces::new();
let mut builder = Element::builder(qname.1, qname.0);
for ((namespace, name), value) in attrs.into_iter() {
if namespace.is_none() {
builder = builder.attr(name, String::from(value));
} else {
let (is_new, prefix) = prefixes.declare_with_auto_prefix(namespace.clone());
let name = prefix.with_suffix(&name);
if is_new {
builder = builder
.prefix(
Some(prefix.as_str().to_owned()),
namespace.as_str().to_owned(),
)
.unwrap();
}
builder = builder.attr(name, String::from(value));
}
}
let element = builder.build();
Ok(Self::Builder {
inner: Some(element),
nested: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transform_element_is_equivalent() {
let el: Element = "<foo xmlns='urn:a' a='b' c='d'><child a='x'/><child a='y'>some text</child><child xmlns='urn:b'><nested-child/></child></foo>".parse().unwrap();
let transformed: Element = crate::transform(el.clone()).unwrap();
assert_eq!(el, transformed);
}
}