xso: add support for fallible parsing and serialisation

This commit is contained in:
Jonas Schäfer 2024-08-10 09:11:32 +02:00
commit 2ff81b3923
4 changed files with 481 additions and 0 deletions

View file

@ -1633,3 +1633,42 @@ fn dynamic_enum_roundtrip_b() {
};
roundtrip_full::<DynamicEnum>("<b xmlns='urn:example:ns2'>hello world</b>");
}
#[derive(FromXml, Debug)]
#[xml(namespace = NS1, name = "parent")]
struct FallibleParse {
#[xml(child)]
child: ::core::result::Result<RequiredAttribute, ::xso::error::Error>,
}
#[test]
fn fallible_parse_positive_ok() {
#[allow(unused_imports)]
use std::{
option::Option::{None, Some},
result::Result::{Err, Ok},
};
match parse_str::<FallibleParse>("<parent xmlns='urn:example:ns1'><attr foo='bar'/></parent>") {
Ok(FallibleParse {
child: Ok(RequiredAttribute { foo }),
}) => {
assert_eq!(foo, "bar");
}
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn fallible_parse_positive_err() {
#[allow(unused_imports)]
use std::{
option::Option::{None, Some},
result::Result::{Err, Ok},
};
match parse_str::<FallibleParse>("<parent xmlns='urn:example:ns1'><attr/></parent>") {
Ok(FallibleParse { child: Err(e) }) => {
assert!(e.to_string().contains("attribute"));
}
other => panic!("unexpected result: {:?}", other),
}
}

View file

@ -68,6 +68,21 @@ impl<T: AsXml> AsXml for Box<T> {
}
}
/// Emits the items of `T` if `Ok(.)` or returns the error from `E` otherwise.
impl<T: AsXml, E> AsXml for Result<T, E>
where
for<'a> Error: From<&'a E>,
{
type ItemIter<'x> = T::ItemIter<'x> where Self: 'x;
fn as_xml_iter(&self) -> Result<Self::ItemIter<'_>, Error> {
match self {
Self::Ok(v) => Ok(v.as_xml_iter()?),
Self::Err(e) => Err(e.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -13,6 +13,26 @@ use core::fmt;
use rxml::error::XmlError;
/// Opaque string error.
///
/// This is exclusively used in the `From<&Error> for Error` implementation
/// in order to type-erase and "clone" the TextParseError.
///
/// That implementation, in turn, is primarily used by the
/// `AsXml for Result<T, E>` implementation. We intentionally do not implement
/// `Clone` using this type because it'd lose type information (which you
/// don't expect a clone to do).
#[derive(Debug)]
struct OpaqueError(String);
impl fmt::Display for OpaqueError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for OpaqueError {}
/// Error variants generated while parsing or serialising XML data.
#[derive(Debug)]
pub enum Error {
@ -43,6 +63,22 @@ impl Error {
}
}
/// "Clone" an [`Error`] while discarding some information.
///
/// This discards the specific type information from the
/// [`TextParseError`][`Self::TextParseError`] variant and it may discard
/// more information in the future.
impl From<&Error> for Error {
fn from(other: &Error) -> Self {
match other {
Self::XmlError(e) => Self::XmlError(e.clone()),
Self::TextParseError(e) => Self::TextParseError(Box::new(OpaqueError(e.to_string()))),
Self::Other(e) => Self::Other(e),
Self::TypeMismatch => Self::TypeMismatch,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {

View file

@ -58,3 +58,394 @@ impl<T: FromXml> FromXml for Box<T> {
Ok(BoxBuilder(Box::new(T::from_events(name, attrs)?)))
}
}
#[derive(Debug)]
enum FallibleBuilderInner<T: FromEventsBuilder, E> {
Processing { depth: usize, builder: T },
Failed { depth: usize, err: Option<E> },
Done,
}
/// Build a `Result<T, E>` from XML.
///
/// This builder, invoked generally via the [`FromXml`] implementation on
/// `Result<T, E> where T: FromXml, E: From<Error>`, allows to fallably parse
/// an XSO from XML.
///
/// If an error occurs while parsing the XSO, the remaining events which
/// belong to that XSO are discarded. Once all events have been seen, the
/// error is returned as `Err(.)` value.
///
/// If parsing succeeds, the parsed XSO is returned as `Ok(.)` value.
#[derive(Debug)]
pub struct FallibleBuilder<T: FromEventsBuilder, E>(FallibleBuilderInner<T, E>);
impl<T: FromEventsBuilder, E: From<Error>> FromEventsBuilder for FallibleBuilder<T, E> {
type Output = Result<T::Output, E>;
fn feed(&mut self, ev: rxml::Event) -> Result<Option<Self::Output>, Error> {
match self.0 {
FallibleBuilderInner::Processing {
ref mut depth,
ref mut builder,
} => {
let new_depth = match ev {
rxml::Event::StartElement(..) => match depth.checked_add(1) {
// I *think* it is OK to return an err here
// instead of panicking. The reason is that anyone
// who intends to resume processing at the level
// of where we started to parse this thing in case
// of an error either has to:
// - Use this fallible implementation and rely on
// it capturing the error (which we don't in
// this case).
// - Or count the depth themselves, which will
// either fail in the same way, or they use a
// wider type (in which case it's ok).
None => {
self.0 = FallibleBuilderInner::Done;
return Err(Error::Other("maximum XML nesting depth exceeded"));
}
Some(v) => Some(v),
},
// In case of an element end, underflow means that we
// have reached the end of the XSO we wanted to process.
// We handle that case at the end of the outer match's
// body: Either we have returned a value then (good), or,
// if we reach the end there with a new_depth == None,
// something went horribly wrong (and we panic).
rxml::Event::EndElement(..) => depth.checked_sub(1),
// Text and XML declarations have no influence on parsing
// depth.
rxml::Event::XmlDeclaration(..) | rxml::Event::Text(..) => Some(*depth),
};
match builder.feed(ev) {
Ok(Some(v)) => {
self.0 = FallibleBuilderInner::Done;
return Ok(Some(Ok(v)));
}
Ok(None) => {
// continue processing in the next round.
}
Err(e) => {
// We are now officially failed ..
match new_depth {
// .. but we are not done yet, so enter the
// failure backtracking state.
Some(depth) => {
self.0 = FallibleBuilderInner::Failed {
depth,
err: Some(e.into()),
};
return Ok(None);
}
// .. and we are done with parsing, so we return
// the error as value.
None => {
self.0 = FallibleBuilderInner::Done;
return Ok(Some(Err(e.into())));
}
}
}
};
*depth = match new_depth {
Some(v) => v,
None => unreachable!("fallible parsing continued beyond end of element"),
};
// Need more events.
Ok(None)
}
FallibleBuilderInner::Failed {
ref mut depth,
ref mut err,
} => {
*depth = match ev {
rxml::Event::StartElement(..) => match depth.checked_add(1) {
// See above for error return rationale.
None => {
self.0 = FallibleBuilderInner::Done;
return Err(Error::Other("maximum XML nesting depth exceeded"));
}
Some(v) => v,
},
rxml::Event::EndElement(..) => match depth.checked_sub(1) {
Some(v) => v,
None => {
// We are officially done, return a value, switch
// states, and be done with it.
let err = err.take().expect("fallible parsing somehow lost its error");
self.0 = FallibleBuilderInner::Done;
return Ok(Some(Err(err)));
}
},
// Text and XML declarations have no influence on parsing
// depth.
rxml::Event::XmlDeclaration(..) | rxml::Event::Text(..) => *depth,
};
// Need more events
Ok(None)
}
FallibleBuilderInner::Done => {
panic!("FromEventsBuilder called after it returned a value")
}
}
}
}
/// Parsers `T` fallibly. See [`FallibleBuilder`] for details.
impl<T: FromXml, E: From<Error>> FromXml for Result<T, E> {
type Builder = FallibleBuilder<T::Builder, E>;
fn from_events(
name: rxml::QName,
attrs: rxml::AttrMap,
) -> Result<Self::Builder, FromEventsError> {
match T::from_events(name, attrs) {
Ok(builder) => Ok(FallibleBuilder(FallibleBuilderInner::Processing {
depth: 0,
builder,
})),
Err(FromEventsError::Mismatch { name, attrs }) => {
Err(FromEventsError::Mismatch { name, attrs })
}
Err(FromEventsError::Invalid(e)) => Ok(FallibleBuilder(FallibleBuilderInner::Failed {
depth: 0,
err: Some(e.into()),
})),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rxml::{parser::EventMetrics, Event, Namespace, NcName};
macro_rules! null_builder {
($name:ident for $output:ident) => {
#[derive(Debug)]
enum $name {}
impl FromEventsBuilder for $name {
type Output = $output;
fn feed(&mut self, _: Event) -> Result<Option<Self::Output>, Error> {
unreachable!();
}
}
};
}
null_builder!(AlwaysMismatchBuilder for AlwaysMismatch);
null_builder!(InitialErrorBuilder for InitialError);
#[derive(Debug)]
struct AlwaysMismatch;
impl FromXml for AlwaysMismatch {
type Builder = AlwaysMismatchBuilder;
fn from_events(
name: rxml::QName,
attrs: rxml::AttrMap,
) -> Result<Self::Builder, FromEventsError> {
Err(FromEventsError::Mismatch { name, attrs })
}
}
#[derive(Debug)]
struct InitialError;
impl FromXml for InitialError {
type Builder = InitialErrorBuilder;
fn from_events(_: rxml::QName, _: rxml::AttrMap) -> Result<Self::Builder, FromEventsError> {
Err(FromEventsError::Invalid(Error::Other("some error")))
}
}
#[derive(Debug)]
struct FailOnContentBuilder;
impl FromEventsBuilder for FailOnContentBuilder {
type Output = FailOnContent;
fn feed(&mut self, _: Event) -> Result<Option<Self::Output>, Error> {
Err(Error::Other("content error"))
}
}
#[derive(Debug)]
struct FailOnContent;
impl FromXml for FailOnContent {
type Builder = FailOnContentBuilder;
fn from_events(_: rxml::QName, _: rxml::AttrMap) -> Result<Self::Builder, FromEventsError> {
Ok(FailOnContentBuilder)
}
}
fn qname() -> rxml::QName {
(Namespace::NONE, NcName::try_from("test").unwrap())
}
fn attrs() -> rxml::AttrMap {
rxml::AttrMap::new()
}
#[test]
fn fallible_builder_missmatch_passthrough() {
match Result::<AlwaysMismatch, Error>::from_events(qname(), attrs()) {
Err(FromEventsError::Mismatch { .. }) => (),
other => panic!("unexpected result: {:?}", other),
}
}
#[test]
fn fallible_builder_initial_error_capture() {
let mut builder = match Result::<InitialError, Error>::from_events(qname(), attrs()) {
Ok(v) => v,
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::EndElement(EventMetrics::zero())) {
Ok(Some(Err(Error::Other("some error")))) => (),
other => panic!("unexpected result: {:?}", other),
};
}
#[test]
fn fallible_builder_initial_error_capture_allows_nested_stuff() {
let mut builder = match Result::<InitialError, Error>::from_events(qname(), attrs()) {
Ok(v) => v,
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::EndElement(EventMetrics::zero())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::EndElement(EventMetrics::zero())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::EndElement(EventMetrics::zero())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::EndElement(EventMetrics::zero())) {
Ok(Some(Err(Error::Other("some error")))) => (),
other => panic!("unexpected result: {:?}", other),
};
}
#[test]
fn fallible_builder_content_error_capture() {
let mut builder = match Result::<FailOnContent, Error>::from_events(qname(), attrs()) {
Ok(v) => v,
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::EndElement(EventMetrics::zero())) {
Ok(Some(Err(Error::Other("content error")))) => (),
other => panic!("unexpected result: {:?}", other),
};
}
#[test]
fn fallible_builder_content_error_capture_with_more_content() {
let mut builder = match Result::<FailOnContent, Error>::from_events(qname(), attrs()) {
Ok(v) => v,
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::EndElement(EventMetrics::zero())) {
Ok(Some(Err(Error::Other("content error")))) => (),
other => panic!("unexpected result: {:?}", other),
};
}
#[test]
fn fallible_builder_content_error_capture_with_nested_content() {
let mut builder = match Result::<FailOnContent, Error>::from_events(qname(), attrs()) {
Ok(v) => v,
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::EndElement(EventMetrics::zero())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::EndElement(EventMetrics::zero())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::EndElement(EventMetrics::zero())) {
Ok(None) => (),
other => panic!("unexpected result: {:?}", other),
};
match builder.feed(Event::EndElement(EventMetrics::zero())) {
Ok(Some(Err(Error::Other("content error")))) => (),
other => panic!("unexpected result: {:?}", other),
};
}
}