xso-proc: add support for defaulting in attribute parsing

This commit is contained in:
Jonas Schäfer 2024-06-26 15:56:43 +02:00
parent cea246a0fc
commit 0c57be3e61
5 changed files with 178 additions and 3 deletions

View file

@ -330,3 +330,53 @@ fn required_non_string_attribute_roundtrip() {
}; };
roundtrip_full::<RequiredNonStringAttribute>("<attr xmlns='urn:example:ns1' foo='-16'/>"); roundtrip_full::<RequiredNonStringAttribute>("<attr xmlns='urn:example:ns1' foo='-16'/>");
} }
#[derive(FromXml, IntoXml, PartialEq, Debug, Clone)]
#[xml(namespace = NS1, name = "attr")]
struct DefaultAttribute {
#[xml(attribute(default))]
foo: std::option::Option<String>,
#[xml(attribute(default))]
bar: std::option::Option<u16>,
}
#[test]
fn default_attribute_roundtrip_aa() {
#[allow(unused_imports)]
use std::{
option::Option::{None, Some},
result::Result::{Err, Ok},
};
roundtrip_full::<DefaultAttribute>("<attr xmlns='urn:example:ns1'/>");
}
#[test]
fn default_attribute_roundtrip_pa() {
#[allow(unused_imports)]
use std::{
option::Option::{None, Some},
result::Result::{Err, Ok},
};
roundtrip_full::<DefaultAttribute>("<attr xmlns='urn:example:ns1' foo='xyz'/>");
}
#[test]
fn default_attribute_roundtrip_ap() {
#[allow(unused_imports)]
use std::{
option::Option::{None, Some},
result::Result::{Err, Ok},
};
roundtrip_full::<DefaultAttribute>("<attr xmlns='urn:example:ns1' bar='16'/>");
}
#[test]
fn default_attribute_roundtrip_pp() {
#[allow(unused_imports)]
use std::{
option::Option::{None, Some},
result::Result::{Err, Ok},
};
roundtrip_full::<DefaultAttribute>("<attr xmlns='urn:example:ns1' foo='xyz' bar='16'/>");
}

View file

@ -13,9 +13,9 @@ use syn::{spanned::Spanned, *};
use rxml_validation::NcName; use rxml_validation::NcName;
use crate::error_message::{self, ParentRef}; use crate::error_message::{self, ParentRef};
use crate::meta::{NameRef, NamespaceRef, XmlFieldMeta}; use crate::meta::{Flag, NameRef, NamespaceRef, XmlFieldMeta};
use crate::scope::{FromEventsScope, IntoEventsScope}; use crate::scope::{FromEventsScope, IntoEventsScope};
use crate::types::{from_xml_text_fn, into_optional_xml_text_fn}; use crate::types::{default_fn, from_xml_text_fn, into_optional_xml_text_fn};
/// Code slices necessary for declaring and initializing a temporary variable /// Code slices necessary for declaring and initializing a temporary variable
/// for parsing purposes. /// for parsing purposes.
@ -67,6 +67,10 @@ enum FieldKind {
/// The XML name of the attribute. /// The XML name of the attribute.
xml_name: NameRef, xml_name: NameRef,
// Flag indicating whether the value should be defaulted if the
// attribute is absent.
default_: Flag,
}, },
} }
@ -81,6 +85,7 @@ impl FieldKind {
span, span,
namespace, namespace,
name, name,
default_,
} => { } => {
let xml_name = match name { let xml_name = match name {
Some(v) => v, Some(v) => v,
@ -107,6 +112,7 @@ impl FieldKind {
Ok(Self::Attribute { Ok(Self::Attribute {
xml_name, xml_name,
xml_namespace: namespace, xml_namespace: namespace,
default_,
}) })
} }
} }
@ -181,6 +187,7 @@ impl FieldDef {
FieldKind::Attribute { FieldKind::Attribute {
ref xml_name, ref xml_name,
ref xml_namespace, ref xml_namespace,
ref default_,
} => { } => {
let FromEventsScope { ref attrs, .. } = scope; let FromEventsScope { ref attrs, .. } = scope;
let ty = self.ty.clone(); let ty = self.ty.clone();
@ -196,12 +203,24 @@ impl FieldDef {
let from_xml_text = from_xml_text_fn(ty.clone()); let from_xml_text = from_xml_text_fn(ty.clone());
let on_absent = match default_ {
Flag::Absent => quote! {
return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into())
},
Flag::Present(_) => {
let default_ = default_fn(ty.clone());
quote! {
#default_()
}
}
};
return Ok(FieldBuilderPart::Init { return Ok(FieldBuilderPart::Init {
value: FieldTempInit { value: FieldTempInit {
init: quote! { init: quote! {
match #attrs.remove(#xml_namespace, #xml_name).map(#from_xml_text).transpose()? { match #attrs.remove(#xml_namespace, #xml_name).map(#from_xml_text).transpose()? {
::core::option::Option::Some(v) => v, ::core::option::Option::Some(v) => v,
::core::option::Option::None => return ::core::result::Result::Err(::xso::error::Error::Other(#missing_msg).into()), ::core::option::Option::None => #on_absent,
} }
}, },
ty: self.ty.clone(), ty: self.ty.clone(),
@ -224,6 +243,7 @@ impl FieldDef {
FieldKind::Attribute { FieldKind::Attribute {
ref xml_name, ref xml_name,
ref xml_namespace, ref xml_namespace,
..
} => { } => {
let IntoEventsScope { ref attrs, .. } = scope; let IntoEventsScope { ref attrs, .. } = scope;
@ -237,6 +257,9 @@ impl FieldDef {
let into_optional_xml_text = into_optional_xml_text_fn(self.ty.clone()); let into_optional_xml_text = into_optional_xml_text_fn(self.ty.clone());
return Ok(FieldIteratorPart::Header { return Ok(FieldIteratorPart::Header {
// This is a neat little trick:
// Option::from(x) converts x to an Option<T> *unless* it
// already is an Option<_>.
setter: quote! { setter: quote! {
#into_optional_xml_text(#bound_name)?.and_then(|#bound_name| #attrs.insert( #into_optional_xml_text(#bound_name)?.and_then(|#bound_name| #attrs.insert(
#xml_namespace, #xml_namespace,

View file

@ -108,6 +108,39 @@ impl quote::ToTokens for NameRef {
} }
} }
/// Represents a boolean flag from a `#[xml(..)]` attribute meta.
#[derive(Clone, Copy, Debug)]
pub(crate) enum Flag {
/// The flag is not set.
Absent,
/// The flag was set.
Present(
/// The span of the syntax element which enabled the flag.
///
/// This is used to generate useful error messages by pointing at the
/// specific place the flag was activated.
#[allow(dead_code)]
Span,
),
}
impl Flag {
/// Return true if the flag is set, false otherwise.
pub(crate) fn is_set(&self) -> bool {
match self {
Self::Absent => false,
Self::Present(_) => true,
}
}
}
impl<T: Spanned> From<T> for Flag {
fn from(other: T) -> Flag {
Flag::Present(other.span())
}
}
/// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum. /// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum.
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct XmlCompoundMeta { pub(crate) struct XmlCompoundMeta {
@ -261,6 +294,9 @@ pub(crate) enum XmlFieldMeta {
/// The XML name supplied. /// The XML name supplied.
name: Option<NameRef>, name: Option<NameRef>,
/// The `default` flag.
default_: Flag,
}, },
} }
@ -279,11 +315,13 @@ impl XmlFieldMeta {
span: meta.path.span(), span: meta.path.span(),
name: Some(name), name: Some(name),
namespace, namespace,
default_: Flag::Absent,
}) })
} else if meta.input.peek(syn::token::Paren) { } else if meta.input.peek(syn::token::Paren) {
// full syntax // full syntax
let mut name: Option<NameRef> = None; let mut name: Option<NameRef> = None;
let mut namespace: Option<NamespaceRef> = None; let mut namespace: Option<NamespaceRef> = None;
let mut default_ = Flag::Absent;
meta.parse_nested_meta(|meta| { meta.parse_nested_meta(|meta| {
if meta.path.is_ident("name") { if meta.path.is_ident("name") {
if name.is_some() { if name.is_some() {
@ -312,6 +350,12 @@ impl XmlFieldMeta {
} }
namespace = Some(meta.value()?.parse()?); namespace = Some(meta.value()?.parse()?);
Ok(()) Ok(())
} else if meta.path.is_ident("default") {
if default_.is_set() {
return Err(Error::new_spanned(meta.path, "duplicate `default` key"));
}
default_ = (&meta.path).into();
Ok(())
} else { } else {
Err(Error::new_spanned(meta.path, "unsupported key")) Err(Error::new_spanned(meta.path, "unsupported key"))
} }
@ -320,6 +364,7 @@ impl XmlFieldMeta {
span: meta.path.span(), span: meta.path.span(),
name, name,
namespace, namespace,
default_,
}) })
} else { } else {
// argument-less syntax // argument-less syntax
@ -327,6 +372,7 @@ impl XmlFieldMeta {
span: meta.path.span(), span: meta.path.span(),
name: None, name: None,
namespace: None, namespace: None,
default_: Flag::Absent,
}) })
} }
} }

View file

@ -114,3 +114,44 @@ pub(crate) fn into_optional_xml_text_fn(ty: Type) -> Expr {
}, },
}) })
} }
/// Construct a [`syn::Expr`] referring to
/// `<#of_ty as ::std::default::Default>::default`.
pub(crate) fn default_fn(of_ty: Type) -> Expr {
let span = of_ty.span();
Expr::Path(ExprPath {
attrs: Vec::new(),
qself: Some(QSelf {
lt_token: syn::token::Lt { spans: [span] },
ty: Box::new(of_ty),
position: 3,
as_token: Some(syn::token::As { span }),
gt_token: syn::token::Gt { spans: [span] },
}),
path: Path {
leading_colon: Some(syn::token::PathSep {
spans: [span, span],
}),
segments: [
PathSegment {
ident: Ident::new("std", span),
arguments: PathArguments::None,
},
PathSegment {
ident: Ident::new("default", span),
arguments: PathArguments::None,
},
PathSegment {
ident: Ident::new("Default", span),
arguments: PathArguments::None,
},
PathSegment {
ident: Ident::new("default", span),
arguments: PathArguments::None,
},
]
.into_iter()
.collect(),
},
})
}

View file

@ -28,6 +28,15 @@ syntax construct *meta*.
All key-value pairs interpreted by these derive macros must be wrapped in a All key-value pairs interpreted by these derive macros must be wrapped in a
`#[xml( ... )]` *meta*. `#[xml( ... )]` *meta*.
The values associated with the keys may be of different types, defined as
such:
- *path*: A Rust path, like `some_crate::foo::Bar`. Note that `foo` on its own
is also a path.
- *string literal*: A string literal, like `"hello world!"`.
- flag: Has no value. The key's mere presence has relevance and it must not be
followed by a `=` sign.
### Struct meta ### Struct meta
The following keys are defined on structs: The following keys are defined on structs:
@ -72,6 +81,7 @@ The following keys can be used inside the `#[xml(attribute(..))]` meta:
| --- | --- | --- | | --- | --- | --- |
| `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`. Note that attributes, unlike elements, are unnamespaced by default. | | `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`. Note that attributes, unlike elements, are unnamespaced by default. |
| `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`. | | `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`. |
| `default` | flag | If present, an absent attribute will substitute the default value instead of raising an error. |
If the `name` key contains a namespace prefix, it must be one of the prefixes If the `name` key contains a namespace prefix, it must be one of the prefixes
defined as built-in in the XML specifications. That prefix will then be defined as built-in in the XML specifications. That prefix will then be
@ -84,6 +94,11 @@ The `attribute` meta also supports a shorthand syntax,
`name` key (with optional prefix as described above, and unnamespaced `name` key (with optional prefix as described above, and unnamespaced
otherwise). otherwise).
If `default` is specified and the attribute is absent in the source, the value
is generated using [`std::default::Default`], requiring the field type to
implement the `Default` trait for a `FromXml` derivation. `default` has no
influence on `IntoXml`.
##### Example ##### Example
```rust ```rust