xso: add support for fallible parsing and serialisation
This commit is contained in:
parent
02a928a47c
commit
2ff81b3923
4 changed files with 481 additions and 0 deletions
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue