From 0c57be3e61c82346fc7e571e4ee4ddbdb362271e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Sch=C3=A4fer?= Date: Wed, 26 Jun 2024 15:56:43 +0200 Subject: [PATCH] xso-proc: add support for defaulting in attribute parsing --- parsers/src/util/macro_tests.rs | 50 +++++++++++++++++++++++++++++++++ xso-proc/src/field.rs | 29 +++++++++++++++++-- xso-proc/src/meta.rs | 46 ++++++++++++++++++++++++++++++ xso-proc/src/types.rs | 41 +++++++++++++++++++++++++++ xso/src/from_xml_doc.md | 15 ++++++++++ 5 files changed, 178 insertions(+), 3 deletions(-) diff --git a/parsers/src/util/macro_tests.rs b/parsers/src/util/macro_tests.rs index 3b5ad69..6ddae99 100644 --- a/parsers/src/util/macro_tests.rs +++ b/parsers/src/util/macro_tests.rs @@ -330,3 +330,53 @@ fn required_non_string_attribute_roundtrip() { }; roundtrip_full::(""); } + +#[derive(FromXml, IntoXml, PartialEq, Debug, Clone)] +#[xml(namespace = NS1, name = "attr")] +struct DefaultAttribute { + #[xml(attribute(default))] + foo: std::option::Option, + + #[xml(attribute(default))] + bar: std::option::Option, +} + +#[test] +fn default_attribute_roundtrip_aa() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::(""); +} + +#[test] +fn default_attribute_roundtrip_pa() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::(""); +} + +#[test] +fn default_attribute_roundtrip_ap() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::(""); +} + +#[test] +fn default_attribute_roundtrip_pp() { + #[allow(unused_imports)] + use std::{ + option::Option::{None, Some}, + result::Result::{Err, Ok}, + }; + roundtrip_full::(""); +} diff --git a/xso-proc/src/field.rs b/xso-proc/src/field.rs index 13f2145..f84f8bd 100644 --- a/xso-proc/src/field.rs +++ b/xso-proc/src/field.rs @@ -13,9 +13,9 @@ use syn::{spanned::Spanned, *}; use rxml_validation::NcName; 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::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 /// for parsing purposes. @@ -67,6 +67,10 @@ enum FieldKind { /// The XML name of the attribute. 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, namespace, name, + default_, } => { let xml_name = match name { Some(v) => v, @@ -107,6 +112,7 @@ impl FieldKind { Ok(Self::Attribute { xml_name, xml_namespace: namespace, + default_, }) } } @@ -181,6 +187,7 @@ impl FieldDef { FieldKind::Attribute { ref xml_name, ref xml_namespace, + ref default_, } => { let FromEventsScope { ref attrs, .. } = scope; let ty = self.ty.clone(); @@ -196,12 +203,24 @@ impl FieldDef { 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 { value: FieldTempInit { init: quote! { match #attrs.remove(#xml_namespace, #xml_name).map(#from_xml_text).transpose()? { ::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(), @@ -224,6 +243,7 @@ impl FieldDef { FieldKind::Attribute { ref xml_name, ref xml_namespace, + .. } => { let IntoEventsScope { ref attrs, .. } = scope; @@ -237,6 +257,9 @@ impl FieldDef { let into_optional_xml_text = into_optional_xml_text_fn(self.ty.clone()); return Ok(FieldIteratorPart::Header { + // This is a neat little trick: + // Option::from(x) converts x to an Option *unless* it + // already is an Option<_>. setter: quote! { #into_optional_xml_text(#bound_name)?.and_then(|#bound_name| #attrs.insert( #xml_namespace, diff --git a/xso-proc/src/meta.rs b/xso-proc/src/meta.rs index edac44d..53bd259 100644 --- a/xso-proc/src/meta.rs +++ b/xso-proc/src/meta.rs @@ -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 From for Flag { + fn from(other: T) -> Flag { + Flag::Present(other.span()) + } +} + /// Contents of an `#[xml(..)]` attribute on a struct, enum variant, or enum. #[derive(Debug)] pub(crate) struct XmlCompoundMeta { @@ -261,6 +294,9 @@ pub(crate) enum XmlFieldMeta { /// The XML name supplied. name: Option, + + /// The `default` flag. + default_: Flag, }, } @@ -279,11 +315,13 @@ impl XmlFieldMeta { span: meta.path.span(), name: Some(name), namespace, + default_: Flag::Absent, }) } else if meta.input.peek(syn::token::Paren) { // full syntax let mut name: Option = None; let mut namespace: Option = None; + let mut default_ = Flag::Absent; meta.parse_nested_meta(|meta| { if meta.path.is_ident("name") { if name.is_some() { @@ -312,6 +350,12 @@ impl XmlFieldMeta { } namespace = Some(meta.value()?.parse()?); 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 { Err(Error::new_spanned(meta.path, "unsupported key")) } @@ -320,6 +364,7 @@ impl XmlFieldMeta { span: meta.path.span(), name, namespace, + default_, }) } else { // argument-less syntax @@ -327,6 +372,7 @@ impl XmlFieldMeta { span: meta.path.span(), name: None, namespace: None, + default_: Flag::Absent, }) } } diff --git a/xso-proc/src/types.rs b/xso-proc/src/types.rs index 358d532..d2c7608 100644 --- a/xso-proc/src/types.rs +++ b/xso-proc/src/types.rs @@ -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(), + }, + }) +} diff --git a/xso/src/from_xml_doc.md b/xso/src/from_xml_doc.md index d9dbd3c..283a8fa 100644 --- a/xso/src/from_xml_doc.md +++ b/xso/src/from_xml_doc.md @@ -28,6 +28,15 @@ syntax construct *meta*. All key-value pairs interpreted by these derive macros must be wrapped in a `#[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 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. | | `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 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 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 ```rust