xso: add support for fallible parsing and serialisation

Jonas SchΓ€fer created

Change summary

parsers/src/util/macro_tests.rs |  39 +++
xso/src/asxml.rs                |  15 +
xso/src/error.rs                |  36 +++
xso/src/fromxml.rs              | 391 +++++++++++++++++++++++++++++++++++
4 files changed, 481 insertions(+)

Detailed changes

parsers/src/util/macro_tests.rs πŸ”—

@@ -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),
+    }
+}

xso/src/asxml.rs πŸ”—

@@ -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::*;

xso/src/error.rs πŸ”—

@@ -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 {

xso/src/fromxml.rs πŸ”—

@@ -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),
+        };
+    }
+}