Up to now, the xmpp-rs projects have been very strict about incoming
data. This has served us, as developers of the libraries, well,
uncovering bugs in our and remote implementations which we could then
get fixed.
However, this behaviour is unexpected to users of the library. In the
XMPP world, unexpected child elements and attributes are generally
expected to be ignored. While this could be opted-into previously, the
feature flag for that sounded more dangerous than it was
("disable-validation"). In addition, the tribal knowledge needed to know
about that feature flag may not have reached some people who tried the
library and gave up because of that.
With this change, we make the non-pedantic behaviour the default. For
development and debugging purposes, users can always opt into the
pedantic behaviour as needed, using the newly-introduced `pedantic`
feature flags on all affected crates.
It was broken in multiple ways:
- xso did not honour it: unknown children and attributes would cause a
parse error even with `--features disable-validation` set on parsers.
For this, we introduce a new feature flag on xso, `non-pedantic`,
which defaults unknown children and attributes to discard instead of
fail.
Note that individual XSOs can still choose to be always pedantic or
always lenient by explicitly declaring the intent via the
`on_unknown_child` and `on_unknown_attribute` metas.
- Many tests in `xmpp_parsers` were broken with `--features
disable-validation`. They now all pass while *still* being rn with
`disable-validation` set: In that case, they test that parsing in fact
succeeds.
Let’s continue reexporting jid and minidom, but not their inner pub
items, users of this crate can go one level deeper if they need that.
Only xso::error::Error is still useful to reexport, as this is part of
the public API of all of our parsers.
Well, not really, of course. All of this will make sense once we start
adding support for fields and non-struct types. Refactoring the code now
before we start to add actual member field parsing is much easier.
How do I know that this will work out? Well, my crystal ball knows it.
Don't believe me? Okay, ChatGPT told me ... Alright alright, I went
through the entire process of implementing this feature *twice* at this
point and have a pretty good idea of where to draw the abstraction lines
so that everything falls neatly into place. You'll have to trust me on
this one.
(Or, you know, check out old branches in my xmpp-rs repo. That might
work, too. `feature/derive-macro-streaming-full` might be a name to look
for if you dare.)
This is a large change and as such, it needs good motivation. Let me
remind you of the ultimate goal: we want a derive macro which allows us
to FromXml/IntoXml, and that derive macro should be usable from
`xmpp_parsers` and other crates.
For that, any code generated by the derive macro mustn't depend on any
code in the `xmpp_parsers` crate, because you cannot name the crate you
are in portably (`xmpp_parsers::..` wouldn't resolve within
`xmpp_parsers`, and `crate::..` would point at other crates if the macro
was used in other crates).
We also want to interoperate with code already implementing
`TryFrom<Element>` and `Into<Element>` on structs. This ultimately
requires that we have an error type which is shared by the two
implementations and that error type must be declared in the `xso` crate
to be usable by the macros.
Thus, we port the error type over to use the type declared in `xso`.
This changes the structure of the error type greatly; I do not think
that `xso` should have to know about all the different types we are
parsing there and they don't deserve special treatment. Wrapping them in
a `Box<dyn ..>` seems more appropriate.
This allows constructs like:
```rust
let residual = match Iq::try_from(stanza) {
Ok(iq) => return handle_iq(..),
Err(Error::TypeMismatch(_, _, v)) => v,
Err(other) => return handle_parse_error(..),
};
let residual = match Message::try_from(stanza) {
..
};
let residual = ..
log::warn!("unhandled object: {:?}", residual);
```
The interesting part of this is that this could be used in a loop over a
Vec<Box<dyn FnMut(Element) -> ControlFlow<SomeResult, Element>>, i.e. in
a parsing loop for a generic XML/XMPP stream.
The advantage is that the stanza.is() check runs only once (in
check_self!) and doesn't need to be duplicated outside, and it reduces
the use of magic strings.