diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index b9dc4c2480649b699b198a7b1cb4c818de877d40..ea7bf3eae25e1f7aa1d77a639ba7bba4f1fd7459 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -1633,3 +1633,42 @@ fn dynamic_enum_roundtrip_b() { }; roundtrip_full::("hello world"); } + +#[derive(FromXml, Debug)] +#[xml(namespace = NS1, name = "parent")] +struct FallibleParse { + #[xml(child)] + child: ::core::result::Result, +} + +#[test] +fn fallible_parse_positive_ok() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + match parse_str::("") { + 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::("") { + Ok(FallibleParse { child: Err(e) }) => { + assert!(e.to_string().contains("attribute")); + } + other => panic!("unexpected result: {:?}", other), + } +} diff --git a/xso/src/asxml.rs b/xso/src/asxml.rs index 21eee74c0563c7068b477cb1f044d46e37c6262b..3d15bd91d4ac2edc0976025b0d6ea5e3687bd827 100644 --- a/xso/src/asxml.rs +++ b/xso/src/asxml.rs @@ -68,6 +68,21 @@ impl AsXml for Box { } } +/// Emits the items of `T` if `Ok(.)` or returns the error from `E` otherwise. +impl AsXml for Result +where + for<'a> Error: From<&'a E>, +{ + type ItemIter<'x> = T::ItemIter<'x> where Self: 'x; + + fn as_xml_iter(&self) -> Result, Error> { + match self { + Self::Ok(v) => Ok(v.as_xml_iter()?), + Self::Err(e) => Err(e.into()), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/xso/src/error.rs b/xso/src/error.rs index d8cebf8c9c6ca0a5e6e03e76063827e5f03d221b..609d08eb4239b500c3ce9a7a34dd3a3ae878ea2d 100644 --- a/xso/src/error.rs +++ b/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` 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 { diff --git a/xso/src/fromxml.rs b/xso/src/fromxml.rs index f76604400c775b297cf9b8a1f28bf2add760334d..3c07f6278dfba2a19a2dcd3b32a7a40817c76e33 100644 --- a/xso/src/fromxml.rs +++ b/xso/src/fromxml.rs @@ -58,3 +58,394 @@ impl FromXml for Box { Ok(BoxBuilder(Box::new(T::from_events(name, attrs)?))) } } + +#[derive(Debug)] +enum FallibleBuilderInner { + Processing { depth: usize, builder: T }, + Failed { depth: usize, err: Option }, + Done, +} + +/// Build a `Result` from XML. +/// +/// This builder, invoked generally via the [`FromXml`] implementation on +/// `Result where T: FromXml, E: From`, 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(FallibleBuilderInner); + +impl> FromEventsBuilder for FallibleBuilder { + type Output = Result; + + fn feed(&mut self, ev: rxml::Event) -> Result, 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> FromXml for Result { + type Builder = FallibleBuilder; + + fn from_events( + name: rxml::QName, + attrs: rxml::AttrMap, + ) -> Result { + 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, 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 { + Err(FromEventsError::Mismatch { name, attrs }) + } + } + + #[derive(Debug)] + struct InitialError; + + impl FromXml for InitialError { + type Builder = InitialErrorBuilder; + + fn from_events(_: rxml::QName, _: rxml::AttrMap) -> Result { + Err(FromEventsError::Invalid(Error::Other("some error"))) + } + } + + #[derive(Debug)] + struct FailOnContentBuilder; + + impl FromEventsBuilder for FailOnContentBuilder { + type Output = FailOnContent; + + fn feed(&mut self, _: Event) -> Result, 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 { + 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::::from_events(qname(), attrs()) { + Err(FromEventsError::Mismatch { .. }) => (), + other => panic!("unexpected result: {:?}", other), + } + } + + #[test] + fn fallible_builder_initial_error_capture() { + let mut builder = match Result::::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::::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::::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::::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::::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), + }; + } +}