Implement #[xml(flag)] meta
This commit is contained in:
parent
4eff6303dc
commit
727e57b756
10 changed files with 423 additions and 2 deletions
|
|
@ -1922,3 +1922,45 @@ fn extract_ignore_unknown_stuff_roundtrip() {
|
|||
};
|
||||
roundtrip_full::<ExtractIgnoreUnknownStuff>("<parent xmlns='urn:example:ns1'><child><grandchild>hello world</grandchild></child></parent>")
|
||||
}
|
||||
|
||||
#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
|
||||
#[xml(namespace = NS1, name = "foo")]
|
||||
struct Flag {
|
||||
#[xml(flag(namespace = NS1, name = "flag"))]
|
||||
flag: bool,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flag_parse_present_as_true() {
|
||||
#[allow(unused_imports)]
|
||||
use core::{
|
||||
option::Option::{None, Some},
|
||||
result::Result::{Err, Ok},
|
||||
};
|
||||
match parse_str::<Flag>("<foo xmlns='urn:example:ns1'><flag/></foo>") {
|
||||
Ok(Flag { flag }) => {
|
||||
assert!(flag);
|
||||
}
|
||||
other => panic!("unexpected result: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flag_present_roundtrip() {
|
||||
#[allow(unused_imports)]
|
||||
use core::{
|
||||
option::Option::{None, Some},
|
||||
result::Result::{Err, Ok},
|
||||
};
|
||||
roundtrip_full::<Flag>("<foo xmlns='urn:example:ns1'><flag/></foo>");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flag_absent_roundtrip() {
|
||||
#[allow(unused_imports)]
|
||||
use core::{
|
||||
option::Option::{None, Some},
|
||||
result::Result::{Err, Ok},
|
||||
};
|
||||
roundtrip_full::<Flag>("<foo xmlns='urn:example:ns1'/>");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ impl ParentRef {
|
|||
/// It implements [`core::fmt::Display`] for that purpose and is otherwise of
|
||||
/// little use.
|
||||
#[repr(transparent)]
|
||||
struct FieldName<'x>(&'x Member);
|
||||
pub(crate) struct FieldName<'x>(pub &'x Member);
|
||||
|
||||
impl fmt::Display for FieldName<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
|
|
|||
137
xso-proc/src/field/flag.rs
Normal file
137
xso-proc/src/field/flag.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright (c) 2024 Jonas Schäfer <jonas@zombofant.net>
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
//! This module concerns the processing of flag-style children.
|
||||
//!
|
||||
//! In particular, it provides the `#[xml(flag)]` implementation.
|
||||
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::*;
|
||||
|
||||
use crate::error_message::{FieldName, ParentRef};
|
||||
use crate::meta::{NameRef, NamespaceRef};
|
||||
use crate::scope::{AsItemsScope, FromEventsScope};
|
||||
use crate::types::{bool_ty, empty_builder_ty, u8_ty};
|
||||
|
||||
use super::{Field, FieldBuilderPart, FieldIteratorPart, FieldTempInit, NestedMatcher};
|
||||
|
||||
/// The field maps to a child element, the presence of which is represented as boolean.
|
||||
pub(super) struct FlagField {
|
||||
/// The XML namespace of the child element.
|
||||
pub(super) xml_namespace: NamespaceRef,
|
||||
|
||||
/// The XML name of the child element.
|
||||
pub(super) xml_name: NameRef,
|
||||
}
|
||||
|
||||
impl Field for FlagField {
|
||||
fn make_builder_part(
|
||||
&self,
|
||||
scope: &FromEventsScope,
|
||||
container_name: &ParentRef,
|
||||
member: &Member,
|
||||
_ty: &Type,
|
||||
) -> Result<FieldBuilderPart> {
|
||||
let field_access = scope.access_field(member);
|
||||
|
||||
let unknown_attr_err = format!(
|
||||
"Unknown attribute in flag child {} in {}.",
|
||||
FieldName(&member),
|
||||
container_name
|
||||
);
|
||||
let unknown_child_err = format!(
|
||||
"Unknown child in flag child {} in {}.",
|
||||
FieldName(&member),
|
||||
container_name
|
||||
);
|
||||
let unknown_text_err = format!(
|
||||
"Unexpected text in flag child {} in {}.",
|
||||
FieldName(&member),
|
||||
container_name
|
||||
);
|
||||
|
||||
let xml_namespace = &self.xml_namespace;
|
||||
let xml_name = &self.xml_name;
|
||||
|
||||
Ok(FieldBuilderPart::Nested {
|
||||
extra_defs: TokenStream::new(),
|
||||
value: FieldTempInit {
|
||||
ty: bool_ty(Span::call_site()),
|
||||
init: quote! { false },
|
||||
},
|
||||
matcher: NestedMatcher::Selective(quote! {
|
||||
if name.0 == #xml_namespace && name.1 == #xml_name {
|
||||
::xso::fromxml::Empty {
|
||||
attributeerr: #unknown_attr_err,
|
||||
childerr: #unknown_child_err,
|
||||
texterr: #unknown_text_err,
|
||||
}.start(attrs).map_err(
|
||||
::xso::error::FromEventsError::Invalid
|
||||
)
|
||||
} else {
|
||||
::core::result::Result::Err(::xso::error::FromEventsError::Mismatch {
|
||||
name,
|
||||
attrs,
|
||||
})
|
||||
}
|
||||
}),
|
||||
builder: empty_builder_ty(Span::call_site()),
|
||||
collect: quote! {
|
||||
#field_access = true;
|
||||
},
|
||||
finalize: quote! {
|
||||
#field_access
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn make_iterator_part(
|
||||
&self,
|
||||
_scope: &AsItemsScope,
|
||||
_container_name: &ParentRef,
|
||||
bound_name: &Ident,
|
||||
_member: &Member,
|
||||
_ty: &Type,
|
||||
) -> Result<FieldIteratorPart> {
|
||||
let xml_namespace = &self.xml_namespace;
|
||||
let xml_name = &self.xml_name;
|
||||
|
||||
Ok(FieldIteratorPart::Content {
|
||||
extra_defs: TokenStream::new(),
|
||||
value: FieldTempInit {
|
||||
init: quote! {
|
||||
if *#bound_name {
|
||||
3
|
||||
} else {
|
||||
1
|
||||
}
|
||||
},
|
||||
ty: u8_ty(Span::call_site()),
|
||||
},
|
||||
generator: quote! {
|
||||
{
|
||||
// using wrapping_sub will make the match below crash
|
||||
// with unreachable!() in case we messed up somewhere.
|
||||
#bound_name = #bound_name.wrapping_sub(1);
|
||||
match #bound_name {
|
||||
0 => ::core::result::Result::<_, ::xso::error::Error>::Ok(::core::option::Option::None),
|
||||
1 => ::core::result::Result::Ok(::core::option::Option::Some(
|
||||
::xso::Item::ElementFoot
|
||||
)),
|
||||
2 => ::core::result::Result::Ok(::core::option::Option::Some(
|
||||
::xso::Item::ElementHeadStart(
|
||||
::xso::exports::rxml::Namespace::from(#xml_namespace),
|
||||
::std::borrow::Cow::Borrowed(#xml_name),
|
||||
)
|
||||
)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -20,12 +20,14 @@ mod attribute;
|
|||
mod child;
|
||||
#[cfg(feature = "minidom")]
|
||||
mod element;
|
||||
mod flag;
|
||||
mod text;
|
||||
|
||||
use self::attribute::AttributeField;
|
||||
use self::child::{ChildField, ExtractDef};
|
||||
#[cfg(feature = "minidom")]
|
||||
use self::element::ElementField;
|
||||
use self::flag::FlagField;
|
||||
use self::text::TextField;
|
||||
|
||||
/// Code slices necessary for declaring and initializing a temporary variable
|
||||
|
|
@ -406,6 +408,18 @@ fn new_field(
|
|||
"#[xml(element)] requires xso to be built with the \"minidom\" feature.",
|
||||
))
|
||||
}
|
||||
|
||||
XmlFieldMeta::Flag {
|
||||
span,
|
||||
qname: QNameRef { namespace, name },
|
||||
} => {
|
||||
let xml_namespace = namespace.unwrap_or_else(|| container_namespace.clone());
|
||||
let xml_name = default_name(span, name, field_ident)?;
|
||||
Ok(Box::new(FlagField {
|
||||
xml_namespace,
|
||||
xml_name,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -755,6 +755,17 @@ pub(crate) enum XmlFieldMeta {
|
|||
/// The `n` flag.
|
||||
amount: Option<AmountConstraint>,
|
||||
},
|
||||
|
||||
/// `#[xml(flag)]
|
||||
Flag {
|
||||
/// The span of the `#[xml(flag)]` meta from which this was parsed.
|
||||
///
|
||||
/// This is useful for error messages.
|
||||
span: Span,
|
||||
|
||||
/// The namespace/name keys.
|
||||
qname: QNameRef,
|
||||
},
|
||||
}
|
||||
|
||||
impl XmlFieldMeta {
|
||||
|
|
@ -1017,6 +1028,19 @@ impl XmlFieldMeta {
|
|||
})
|
||||
}
|
||||
|
||||
/// Parse a `#[xml(flag)]` meta.
|
||||
fn flag_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
|
||||
let mut qname = QNameRef::default();
|
||||
meta.parse_nested_meta(|meta| match qname.parse_incremental_from_meta(meta)? {
|
||||
None => Ok(()),
|
||||
Some(meta) => Err(Error::new_spanned(meta.path, "unsupported key")),
|
||||
})?;
|
||||
Ok(Self::Flag {
|
||||
span: meta.path.span(),
|
||||
qname,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse [`Self`] from a nestd meta, switching on the identifier
|
||||
/// of that nested meta.
|
||||
fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
|
||||
|
|
@ -1030,6 +1054,8 @@ impl XmlFieldMeta {
|
|||
Self::extract_from_meta(meta)
|
||||
} else if meta.path.is_ident("element") {
|
||||
Self::element_from_meta(meta)
|
||||
} else if meta.path.is_ident("flag") {
|
||||
Self::flag_from_meta(meta)
|
||||
} else {
|
||||
Err(Error::new_spanned(meta.path, "unsupported field meta"))
|
||||
}
|
||||
|
|
@ -1112,6 +1138,7 @@ impl XmlFieldMeta {
|
|||
Self::Text { ref span, .. } => *span,
|
||||
Self::Extract { ref span, .. } => *span,
|
||||
Self::Element { ref span, .. } => *span,
|
||||
Self::Flag { ref span, .. } => *span,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -890,3 +890,95 @@ pub(crate) fn discard_builder_ty(span: Span) -> Type {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct a [`syn::Type`] referring to the built-in `bool` type.
|
||||
///
|
||||
/// Note that we go through `xso::exports::CoreBool` for that, because there seems
|
||||
/// to be no way to access built-in types once they have been shadowed in a
|
||||
/// scope.
|
||||
pub(crate) fn bool_ty(span: Span) -> Type {
|
||||
Type::Path(TypePath {
|
||||
qself: None,
|
||||
path: Path {
|
||||
leading_colon: Some(syn::token::PathSep {
|
||||
spans: [span, span],
|
||||
}),
|
||||
segments: [
|
||||
PathSegment {
|
||||
ident: Ident::new("xso", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
PathSegment {
|
||||
ident: Ident::new("exports", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
PathSegment {
|
||||
ident: Ident::new("CoreBool", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct a [`syn::Type`] referring to the built-in `u8` type.
|
||||
///
|
||||
/// Note that we go through `xso::exports::CoreU8` for that, because there seems
|
||||
/// to be no way to access built-in types once they have been shadowed in a
|
||||
/// scope.
|
||||
pub(crate) fn u8_ty(span: Span) -> Type {
|
||||
Type::Path(TypePath {
|
||||
qself: None,
|
||||
path: Path {
|
||||
leading_colon: Some(syn::token::PathSep {
|
||||
spans: [span, span],
|
||||
}),
|
||||
segments: [
|
||||
PathSegment {
|
||||
ident: Ident::new("xso", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
PathSegment {
|
||||
ident: Ident::new("exports", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
PathSegment {
|
||||
ident: Ident::new("CoreU8", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct a [`syn::Type`] referring to `::xso::fromxml::EmptyBuilder`.
|
||||
pub(crate) fn empty_builder_ty(span: Span) -> Type {
|
||||
Type::Path(TypePath {
|
||||
qself: None,
|
||||
path: Path {
|
||||
leading_colon: Some(syn::token::PathSep {
|
||||
spans: [span, span],
|
||||
}),
|
||||
segments: [
|
||||
PathSegment {
|
||||
ident: Ident::new("xso", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
PathSegment {
|
||||
ident: Ident::new("fromxml", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
PathSegment {
|
||||
ident: Ident::new("EmptyBuilder", span),
|
||||
arguments: PathArguments::None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ Version NEXT:
|
|||
- Support for "transparent" structs (newtype-like patterns for XSO).
|
||||
- FromXmlText and AsXmlText are now implemented for jid::NodePart,
|
||||
jid::DomainPart, and jid::ResourcePart (!485)
|
||||
- Support for optional child elements, the presence of which are
|
||||
translated into a boolean (`#[xml(flag)]`).
|
||||
|
||||
Version 0.1.2:
|
||||
2024-07-26 Jonas Schäfer <jonas@zombofant.net>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ assert_eq!(foo, Foo);
|
|||
2. [`child` meta](#child-meta)
|
||||
3. [`element` meta](#element-meta)
|
||||
4. [`extract` meta](#extract-meta)
|
||||
5. [`text` meta](#text-meta)
|
||||
5. [`flag` meta](#flag-meta)
|
||||
6. [`text` meta](#text-meta)
|
||||
|
||||
## Attributes
|
||||
|
||||
|
|
@ -557,6 +558,47 @@ assert_eq!(foo, Foo {
|
|||
});
|
||||
```
|
||||
|
||||
### `flag` meta
|
||||
|
||||
The `flag` meta causes the field to be mapped to a single, optional child element. Absence of the child is equivalent to the value `false`, presence
|
||||
of the child element is equivalent to the value `true`.
|
||||
|
||||
The following keys can be used inside the `#[xml(flag(..))]` meta:
|
||||
|
||||
| Key | Value type | Description |
|
||||
| --- | --- | --- |
|
||||
| `namespace` | *string literal* or *path* | The optional namespace of the XML attribute to match. If it is a *path*, it must point at a `&'static str`. |
|
||||
| `name` | *string literal* or *path* | The name of the XML attribute to match. If it is a *path*, it must point at a `&'static NcNameStr`. |
|
||||
|
||||
The field on which the `flag` meta is used must be of type `bool`.
|
||||
|
||||
If `namespace` is not set, it defaults to the namespace of the surrounding
|
||||
container. If `name` is not set, it defaults to the field's name, if available.
|
||||
If `name` is not set and the field is unnamed, a compile-time error is raised.
|
||||
|
||||
When parsing, any contents within the child element generate a parse error.
|
||||
|
||||
#### Example
|
||||
```rust
|
||||
# use xso::FromXml;
|
||||
#[derive(FromXml, Debug, PartialEq)]
|
||||
#[xml(namespace = "urn:example", name = "foo")]
|
||||
struct Foo {
|
||||
#[xml(flag(name = "flag"))]
|
||||
flag: bool,
|
||||
};
|
||||
|
||||
let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example'><flag/></foo>").unwrap();
|
||||
assert_eq!(foo, Foo {
|
||||
flag: true,
|
||||
});
|
||||
|
||||
let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example'/>").unwrap();
|
||||
assert_eq!(foo, Foo {
|
||||
flag: false,
|
||||
});
|
||||
```
|
||||
|
||||
### `text` meta
|
||||
|
||||
The `text` meta causes the field to be mapped to the text content of the
|
||||
|
|
|
|||
|
|
@ -269,6 +269,58 @@ impl FromEventsBuilder for Discard {
|
|||
}
|
||||
}
|
||||
|
||||
/// Builder which discards the contents (or raises on unexpected contents).
|
||||
///
|
||||
/// This builder is only to be used from within the proc macros and is not
|
||||
/// stable, public API.
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "macros")]
|
||||
pub struct EmptyBuilder {
|
||||
childerr: &'static str,
|
||||
texterr: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
impl FromEventsBuilder for EmptyBuilder {
|
||||
type Output = ();
|
||||
|
||||
fn feed(&mut self, ev: rxml::Event) -> Result<Option<Self::Output>, Error> {
|
||||
match ev {
|
||||
rxml::Event::EndElement(..) => Ok(Some(())),
|
||||
rxml::Event::StartElement(..) => Err(Error::Other(self.childerr)),
|
||||
rxml::Event::Text(..) => Err(Error::Other(self.texterr)),
|
||||
_ => Err(Error::Other(
|
||||
"unexpected content in supposed-to-be-empty element",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Precursor struct for [`EmptyBuilder`].
|
||||
///
|
||||
/// This struct is only to be used from within the proc macros and is not
|
||||
/// stable, public API.
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "macros")]
|
||||
pub struct Empty {
|
||||
pub attributeerr: &'static str,
|
||||
pub childerr: &'static str,
|
||||
pub texterr: &'static str,
|
||||
}
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
impl Empty {
|
||||
pub fn start(self, attr: rxml::AttrMap) -> Result<EmptyBuilder, Error> {
|
||||
if attr.len() > 0 {
|
||||
return Err(Error::Other(self.attributeerr));
|
||||
}
|
||||
Ok(EmptyBuilder {
|
||||
childerr: self.childerr,
|
||||
texterr: self.texterr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -39,10 +39,23 @@ mod rxml_util;
|
|||
pub mod text;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "macros")]
|
||||
pub mod exports {
|
||||
#[cfg(feature = "minidom")]
|
||||
pub use minidom;
|
||||
pub use rxml;
|
||||
|
||||
/// The built-in `bool` type.
|
||||
///
|
||||
/// This is re-exported for use by macros in cases where we cannot rely on
|
||||
/// people not having done `type bool = str` or some similar shenanigans.
|
||||
pub type CoreBool = bool;
|
||||
|
||||
/// The built-in `u8` type.
|
||||
///
|
||||
/// 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;
|
||||
}
|
||||
|
||||
use alloc::{
|
||||
|
|
|
|||
Loading…
Reference in a new issue