xso: reject attempts to match the same XML attribute in different fields

This was a bit tricky to build, because it is possible to have an
indirection through a `static` there. Thanks to Rust's extensive
const-fn capabilities, though, it's in fact possible to cover all cases.

We still do two different checks to improve user experience. If we can,
from within the proc macro, determine that two fields refer to the same
XML attribute (because their namespace/name values use the same Rust
tokens), then we reject the fields with a clear error message pointing
at both fields.

In the other case, when there's e.g. `#[xml(lang)]` and
`#[xml(attribute(namespace = rxml::XMLNS_XML, name = "lang"))]`, the
macro cannot be sure that XMLNS_XML is in fact the XML namespace. For
that case, we generate code which is evaluated at compile time (and
has no runtime impact) which panics if the namespace and name of two
attribute-matching fields is the same.

The error message will be less clear (because it contains extra,
unchangeable wording like "evaluation of constant value failed" and "the
evaluated program panicked at", which may be a bit confusing) than the
message generated by the macros themselves, but it's a price we have to
pay unfortunately.

Note that this check may seem cosmetic and purely for better user
experience, but it is in fact needed to avoid generating not-well-formed
and/or not-namespace-well-formed XML: As `AsXml` generates `xso::Item`,
where each attribute is emitted separated (and not aggregated in a
map structure), a naive (and efficient) implementation of a writer might
not double-check that no duplicate attributes are generated.
This commit is contained in:
Jonas Schäfer 2025-04-27 09:26:56 +02:00
commit 9fdb1564f6
10 changed files with 267 additions and 23 deletions

View file

@ -59,6 +59,8 @@ Version NEXT:
field in a struct definition would cause a compile-time error when
deriving `AsXml`.
- Update rxml dependency to 0.13.
- xso now rejects conflicting `#[xml(attribute)]` (and `#[xml(lang)]`)
specifications at compile time.
Version 0.1.2:
2024-07-26 Jonas Schäfer <jonas@zombofant.net>

View file

@ -323,14 +323,6 @@ field type on which the `extract` is declared.
If `codec` is given, the given `codec` value must implement
[`TextCodec<T>`][`TextCodec`] where `T` is the type of the field.
If two (or more) `#[xml(attribute)]` metas match the same XML attribute,
unspecified behavior occurs during serialisation: only one of the values will
be in the output, but it is unspecified which of the two. (Due to indirections
when refering to `static` items for attribute namespaces and names, it is not
possible to check this at compile-time.) This behaviour also affects
attribute fields which match the special `xml:lang` attribute when used in
conjuction with a `#[xml(lang)]` field.
#### Example
```rust
@ -365,6 +357,37 @@ assert_eq!(foo, Foo {
});
```
Note that it is not possible to have two `#[xml(attribute)]` fields which
match the same XML attribute:
```compile_fail
# use xso::FromXml;
#[derive(FromXml)]
#[xml(namespace = "urn:example", name = "dup")]
struct Dup {
#[xml(attribute)]
a: String,
#[xml(attribute = "a")]
b: String,
}
```
```compile_fail
# use xso::FromXml;
static A: &str = "a";
#[derive(FromXml)]
#[xml(namespace = "urn:example", name = "dup")]
struct Dup {
#[xml(attribute)]
a: String,
#[xml(attribute = A)]
b: String,
}
```
### `child` meta
The `child` meta causes the field to be mapped to a child element of the
@ -666,14 +689,6 @@ The `lang` meta allows to access the (potentially inherited) logical
This meta supports no arguments and can only be used on fields of type
`Option<String>`.
This meta should not be used alongsite `#[xml(attribute)]` meta which match
the `xml:lang` attribute. Doing so causes unspecified behavior during
serialisation: only one of the values will be in the output, but it is
unspecified which of the two. This is the same as when having two
`#[xml(attribute)]` field which match the same attribute. (Due to indirections
when refering to `static` items for attribute namespaces and names, it is not
possible to check this at compile-time.)
Unlike `#[xml(attribute = "xml:lang")]`, the `#[xml(lang)]` meta takes
inheritance into account.
@ -715,6 +730,23 @@ assert_eq!(foo, Foo {
});
```
Note that it is not possible to use `#[xml(lang)]` and an `#[xml(attribute)]`
which also matches `xml:lang` in the same struct:
```compile_fail
# use xso::FromXml;
# use xso::exports::rxml::XMLNS_XML;
#[derive(FromXml)]
#[xml(namespace = "urn:example", name = "dup")]
struct Dup {
#[xml(attribute(namespace = XMLNS_XML, name = "lang"))]
a: String,
#[xml(lang)]
b: Option<String>,
}
```
### `text` meta
The `text` meta causes the field to be mapped to the text content of the

View file

@ -72,6 +72,29 @@ pub mod exports {
/// This is re-exported for use by macros in cases where we cannot rely on
/// people not having done `type u8 = str` or some similar shenanigans.
pub type CoreU8 = u8;
/// Compile-time comparison of two strings.
///
/// Used by macro-generated code.
///
/// This is necessary because `<str as PartialEq>::eq` is not `const`.
pub const fn const_str_eq(a: &'static str, b: &'static str) -> bool {
let a = a.as_bytes();
let b = b.as_bytes();
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
}
use alloc::{