Port everything over to AsXml

This commit is contained in:
Jonas Schäfer 2024-07-09 17:01:42 +02:00
commit ccf38cdf9b
64 changed files with 848 additions and 371 deletions

View file

@ -1,7 +1,7 @@
# Make a struct or enum parseable from XML
This derives the [`FromXml`] trait on a struct or enum. It is the counterpart
to [`macro@IntoXml`].
to [`macro@AsXml`].
## Example
@ -75,7 +75,7 @@ The following mapping types are defined:
The `attribute` meta causes the field to be mapped to an XML attribute of the
same name. For `FromXml`, the field's type must implement [`FromXmlText`] and
for `IntoXml`, the field's type must implement [`IntoOptionalXmlText`].
for `AsXml`, the field's type must implement [`AsOptionalXmlText`].
The following keys can be used inside the `#[xml(attribute(..))]` meta:
@ -99,7 +99,7 @@ otherwise).
If `default` is specified and the attribute is absent in the source, the value
is generated using [`std::default::Default`], requiring the field type to
implement the `Default` trait for a `FromXml` derivation. `default` has no
influence on `IntoXml`.
influence on `AsXml`.
##### Example
@ -148,14 +148,14 @@ If `codec` is given, the given `codec` must implement
[`TextCodec<T>`][`TextCodec`] where `T` is the type of the field.
If `codec` is *not* given, the field's type must implement [`FromXmlText`] for
`FromXml` and for `IntoXml`, the field's type must implement [`IntoXmlText`].
`FromXml` and for `AsXml`, the field's type must implement [`AsXmlText`].
The `text` meta also supports a shorthand syntax, `#[xml(text = ..)]`, where
the value is treated as the value for the `codec` key (with optional prefix as
described above, and unnamespaced otherwise).
Only a single field per struct may be annotated with `#[xml(text)]` at a time,
to avoid parsing ambiguities. This is also true if only `IntoXml` is derived on
to avoid parsing ambiguities. This is also true if only `AsXml` is derived on
a field, for consistency.
##### Example without codec

View file

@ -49,14 +49,14 @@ pub use xso_proc::FromXml;
/// # Make a struct or enum serialisable to XML
///
/// This derives the [`IntoXml`] trait on a struct or enum. It is the
/// This derives the [`AsXml`] trait on a struct or enum. It is the
/// counterpart to [`macro@FromXml`].
///
/// The attributes necessary and available for the derivation to work are
/// documented on [`macro@FromXml`].
#[doc(inline)]
#[cfg(feature = "macros")]
pub use xso_proc::IntoXml;
pub use xso_proc::AsXml;
/// Trait allowing to consume a struct and iterate its contents as
/// serialisable [`rxml::Event`] items.
@ -369,10 +369,10 @@ impl<T: AsXmlText> AsOptionalXmlText for Option<T> {
}
}
/// Attempt to transform a type implementing [`IntoXml`] into another
/// Attempt to transform a type implementing [`AsXml`] into another
/// type which implements [`FromXml`].
pub fn transform<T: FromXml, F: IntoXml>(from: F) -> Result<T, self::error::Error> {
let mut iter = from.into_event_iter()?;
pub fn transform<T: FromXml, F: AsXml>(from: F) -> Result<T, self::error::Error> {
let mut iter = self::rxml_util::ItemToEvent::new(from.as_xml_iter()?);
let (qname, attrs) = match iter.next() {
Some(Ok(rxml::Event::StartElement(_, qname, attrs))) => (qname, attrs),
Some(Err(e)) => return Err(e),
@ -418,8 +418,24 @@ pub fn try_from_element<T: FromXml>(
}
};
let mut iter = from.into_event_iter()?;
iter.next().expect("first event from minidom::Element")?;
let mut iter = from.as_xml_iter()?;
// consume the element header
for item in &mut iter {
let item = item?;
match item {
// discard the element header
Item::XmlDeclaration(..) => (),
Item::ElementHeadStart(..) => (),
Item::Attribute(..) => (),
Item::ElementHeadEnd => {
// now that the element header is over, we break out
break;
}
Item::Text(..) => panic!("text before end of element header"),
Item::ElementFoot => panic!("element foot before end of element header"),
}
}
let iter = self::rxml_util::ItemToEvent::new(iter);
for event in iter {
let event = event?;
if let Some(v) = sink.feed(event)? {

View file

@ -8,9 +8,7 @@
use std::borrow::Cow;
use rxml::{Namespace, NcNameStr, XmlVersion};
#[cfg(feature = "minidom")]
use rxml::Event;
use rxml::{parser::EventMetrics, AttrMap, Event, Namespace, NcName, NcNameStr, XmlVersion};
/// An encodable item.
///
@ -164,12 +162,130 @@ impl<I: Iterator<Item = Result<Event, crate::error::Error>>> Iterator for EventT
}
}
/// Iterator adapter which converts an iterator over [`Item`] to
/// an iterator over [`Event`][`crate::Event`].
///
/// As `Event` does not support borrowing data, this iterator copies the data
/// from the items on the fly.
pub(crate) struct ItemToEvent<I> {
inner: I,
event_buffer: Option<Event>,
elem_buffer: Option<(Namespace, NcName, AttrMap)>,
}
impl<'x, I: Iterator<Item = Result<Item<'x>, crate::error::Error>>> ItemToEvent<I> {
/// Create a new adapter with `inner` as the source iterator.
pub(crate) fn new(inner: I) -> Self {
Self {
inner,
event_buffer: None,
elem_buffer: None,
}
}
}
impl<I> ItemToEvent<I> {
fn update<'x>(&mut self, item: Item<'x>) -> Result<Option<Event>, crate::error::Error> {
assert!(self.event_buffer.is_none());
match item {
Item::XmlDeclaration(v) => {
assert!(self.elem_buffer.is_none());
Ok(Some(Event::XmlDeclaration(EventMetrics::zero(), v)))
}
Item::ElementHeadStart(ns, name) => {
if self.elem_buffer.is_some() {
// this is only used with AsXml implementations, so
// triggering this is always a coding failure instead of a
// runtime error.
panic!("got a second ElementHeadStart items without ElementHeadEnd inbetween: ns={:?} name={:?} (state={:?})", ns, name, self.elem_buffer);
}
self.elem_buffer = Some((ns.to_owned(), name.into_owned(), AttrMap::new()));
Ok(None)
}
Item::Attribute(ns, name, value) => {
let Some((_, _, attrs)) = self.elem_buffer.as_mut() else {
// this is only used with AsXml implementations, so
// triggering this is always a coding failure instead of a
// runtime error.
panic!(
"got a second Attribute item without ElementHeadStart: ns={:?}, name={:?}",
ns, name
);
};
attrs.insert(ns, name.into_owned(), value.into_owned());
Ok(None)
}
Item::ElementHeadEnd => {
let Some((ns, name, attrs)) = self.elem_buffer.take() else {
// this is only used with AsXml implementations, so
// triggering this is always a coding failure instead of a
// runtime error.
panic!(
"got ElementHeadEnd item without ElementHeadStart: {:?}",
item
);
};
Ok(Some(Event::StartElement(
EventMetrics::zero(),
(ns, name),
attrs,
)))
}
Item::Text(value) => {
if let Some(elem_buffer) = self.elem_buffer.as_ref() {
// this is only used with AsXml implementations, so
// triggering this is always a coding failure instead of a
// runtime error.
panic!("got Text after ElementHeadStart but before ElementHeadEnd: Text({:?}) (state = {:?})", value, elem_buffer);
}
Ok(Some(Event::Text(EventMetrics::zero(), value.into_owned())))
}
Item::ElementFoot => {
let end_ev = Event::EndElement(EventMetrics::zero());
let result = if let Some((ns, name, attrs)) = self.elem_buffer.take() {
// content-less element
self.event_buffer = Some(end_ev);
Event::StartElement(EventMetrics::zero(), (ns, name), attrs)
} else {
end_ev
};
Ok(Some(result))
}
}
}
}
impl<'x, I: Iterator<Item = Result<Item<'x>, crate::error::Error>>> Iterator for ItemToEvent<I> {
type Item = Result<Event, crate::error::Error>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(event) = self.event_buffer.take() {
return Some(Ok(event));
}
loop {
let item = match self.inner.next() {
Some(Ok(v)) => v,
Some(Err(e)) => return Some(Err(e)),
None => return None,
};
match self.update(item).transpose() {
Some(v) => return Some(v),
None => (),
}
}
}
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>> {
@ -307,3 +423,144 @@ mod tests_minidom {
};
}
}
#[cfg(test)]
mod tests {
use std::convert::TryInto;
use super::*;
fn items_to_events<'x, I: IntoIterator<Item = Item<'x>>>(
items: I,
) -> Result<Vec<Event>, crate::error::Error> {
let iter = ItemToEvent {
inner: items.into_iter().map(|x| Ok(x)),
event_buffer: None,
elem_buffer: None,
};
let mut result = Vec::new();
for ev in iter {
let ev = ev?;
result.push(ev);
}
Ok(result)
}
#[test]
fn item_to_event_xml_decl() {
let items = vec![Item::XmlDeclaration(XmlVersion::V1_0)];
let events = items_to_events(items).expect("item conversion");
assert_eq!(events.len(), 1);
match events[0] {
Event::XmlDeclaration(_, XmlVersion::V1_0) => (),
ref other => panic!("unexected event in position 0: {:?}", other),
};
}
#[test]
fn item_to_event_simple_empty_element() {
let items = vec![
Item::ElementHeadStart(Namespace::NONE, Cow::Borrowed("elem".try_into().unwrap())),
Item::ElementHeadEnd,
Item::ElementFoot,
];
let events = items_to_events(items).expect("item conversion");
assert_eq!(events.len(), 2);
match events[0] {
Event::StartElement(_, (ref ns, ref name), ref attrs) => {
assert_eq!(attrs.len(), 0);
assert_eq!(ns, Namespace::none());
assert_eq!(name, "elem");
}
ref other => panic!("unexected event in position 0: {:?}", other),
};
match events[1] {
Event::EndElement(_) => (),
ref other => panic!("unexected event in position 1: {:?}", other),
};
}
#[test]
fn item_to_event_short_empty_element() {
let items = vec![
Item::ElementHeadStart(Namespace::NONE, Cow::Borrowed("elem".try_into().unwrap())),
Item::ElementFoot,
];
let events = items_to_events(items).expect("item conversion");
assert_eq!(events.len(), 2);
match events[0] {
Event::StartElement(_, (ref ns, ref name), ref attrs) => {
assert_eq!(attrs.len(), 0);
assert_eq!(ns, Namespace::none());
assert_eq!(name, "elem");
}
ref other => panic!("unexected event in position 0: {:?}", other),
};
match events[1] {
Event::EndElement(_) => (),
ref other => panic!("unexected event in position 1: {:?}", other),
};
}
#[test]
fn item_to_event_element_with_text_content() {
let items = vec![
Item::ElementHeadStart(Namespace::NONE, Cow::Borrowed("elem".try_into().unwrap())),
Item::ElementHeadEnd,
Item::Text(Cow::Borrowed("Hello World!")),
Item::ElementFoot,
];
let events = items_to_events(items).expect("item conversion");
assert_eq!(events.len(), 3);
match events[0] {
Event::StartElement(_, (ref ns, ref name), ref attrs) => {
assert_eq!(attrs.len(), 0);
assert_eq!(ns, Namespace::none());
assert_eq!(name, "elem");
}
ref other => panic!("unexected event in position 0: {:?}", other),
};
match events[1] {
Event::Text(_, ref value) => {
assert_eq!(value, "Hello World!");
}
ref other => panic!("unexected event in position 1: {:?}", other),
};
match events[2] {
Event::EndElement(_) => (),
ref other => panic!("unexected event in position 2: {:?}", other),
};
}
#[test]
fn item_to_event_element_with_attributes() {
let items = vec![
Item::ElementHeadStart(Namespace::NONE, Cow::Borrowed("elem".try_into().unwrap())),
Item::Attribute(
Namespace::NONE,
Cow::Borrowed("attr".try_into().unwrap()),
Cow::Borrowed("value"),
),
Item::ElementHeadEnd,
Item::ElementFoot,
];
let events = items_to_events(items).expect("item conversion");
assert_eq!(events.len(), 2);
match events[0] {
Event::StartElement(_, (ref ns, ref name), ref attrs) => {
assert_eq!(ns, Namespace::none());
assert_eq!(name, "elem");
assert_eq!(attrs.len(), 1);
assert_eq!(
attrs.get(Namespace::none(), "attr").map(|x| x.as_str()),
Some("value")
);
}
ref other => panic!("unexected event in position 0: {:?}", other),
};
match events[1] {
Event::EndElement(_) => (),
ref other => panic!("unexected event in position 2: {:?}", other),
};
}
}

View file

@ -134,7 +134,7 @@ convert_via_fromstr_and_display! {
/// Represent a way to encode/decode text data into a Rust type.
///
/// This trait can be used in scenarios where implementing [`FromXmlText`]
/// and/or [`IntoXmlText`] on a type is not feasible or sensible, such as the
/// 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
@ -143,7 +143,7 @@ convert_via_fromstr_and_display! {
/// 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 `IntoXml` derive macros. See the documentation of the
/// by `FromXml` and `AsXml` derive macros. See the documentation of the
/// [`FromXml`][`macro@crate::FromXml`] derive macro for details.
pub trait TextCodec<T> {
/// Decode a string value into the type.
@ -152,7 +152,7 @@ pub trait TextCodec<T> {
/// Encode the type as string value.
///
/// If this returns `None`, the string value is not emitted at all.
fn encode(value: T) -> Result<Option<String>, Error>;
fn encode(value: &T) -> Result<Option<Cow<'_, str>>, Error>;
}
/// Text codec which does no transform.
@ -163,8 +163,8 @@ impl TextCodec<String> for Plain {
Ok(s)
}
fn encode(value: String) -> Result<Option<String>, Error> {
Ok(Some(value))
fn encode(value: &String) -> Result<Option<Cow<'_, str>>, Error> {
Ok(Some(Cow::Borrowed(value.as_str())))
}
}
@ -180,9 +180,9 @@ impl TextCodec<Option<String>> for EmptyAsNone {
}
}
fn encode(value: Option<String>) -> Result<Option<String>, Error> {
Ok(match value {
Some(v) if !v.is_empty() => Some(v),
fn encode(value: &Option<String>) -> Result<Option<Cow<'_, str>>, Error> {
Ok(match value.as_ref() {
Some(v) if !v.is_empty() => Some(Cow::Borrowed(v.as_str())),
Some(_) | None => None,
})
}
@ -237,8 +237,8 @@ impl<Filter: TextFilter> TextCodec<Vec<u8>> for Base64<Filter> {
.map_err(Error::text_parse_error)
}
fn encode(value: Vec<u8>) -> Result<Option<String>, Error> {
Ok(Some(StandardBase64Engine.encode(&value)))
fn encode(value: &Vec<u8>) -> Result<Option<Cow<'_, str>>, Error> {
Ok(Some(Cow::Owned(StandardBase64Engine.encode(&value))))
}
}
@ -252,7 +252,11 @@ impl<Filter: TextFilter> TextCodec<Option<Vec<u8>>> for Base64<Filter> {
Ok(Some(Self::decode(s)?))
}
fn encode(decoded: Option<Vec<u8>>) -> Result<Option<String>, Error> {
decoded.map(Self::encode).transpose().map(Option::flatten)
fn encode(decoded: &Option<Vec<u8>>) -> Result<Option<Cow<'_, str>>, Error> {
decoded
.as_ref()
.map(Self::encode)
.transpose()
.map(Option::flatten)
}
}