xso_proc: unify meta option parsing

That makes it easier to add new options to the various attributes.
This commit is contained in:
Jonas Schäfer 2024-03-25 17:06:11 +01:00
parent e08a403a25
commit c8e03ec1f8
1 changed files with 134 additions and 164 deletions

View File

@ -12,100 +12,129 @@ structs in the [`crate::field`] module, are responsible for ensuring that the
given combinations make sense and emit compile-time errors if they do not.
*/
use std::fmt;
use std::ops::ControlFlow;
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use syn::{spanned::Spanned, *};
/// Concatenate a list of identifiers into a comma-separated string.
/// Helper trait to parse a value of some kind from a
/// [`syn::meta::ParseNestedMeta`].
///
/// Used for generating error messages.
macro_rules! concat_options {
($head:ident, $($field:ident,)+) => {
concat!(stringify!($head), ", ", concat_options!($($field,)+))
};
($last:ident,) => {
stringify!($last)
/// This, combined with the [`parse_meta!`] macro, reduces code duplication
/// in the various parsing functions significantly.
trait MetaParse {
/// The identifier to match against the path of the meta.
fn name(&self) -> &'static str;
/// The actual workhorse: Assuming that the path matches [`Self::name`],
/// parse the data from the meta.
fn force_parse_at_meta<'x>(&mut self, meta: meta::ParseNestedMeta<'x>) -> Result<()>;
/// Test the path against [`Self::name`] and parse the value if the path
/// matches.
///
/// Otherwise, return [`std::ops::ControlFlow::Continue`] with the meta
/// to allow other things to be parsed from it.
fn parse_at_meta<'x>(
&mut self,
meta: meta::ParseNestedMeta<'x>,
) -> Result<ControlFlow<(), meta::ParseNestedMeta<'x>>> {
if !meta.path.is_ident(self.name()) {
return Ok(ControlFlow::Continue(meta));
}
Ok(ControlFlow::Break(self.force_parse_at_meta(meta)?))
}
}
/// Format an "unsupported option" error message.
///
/// The allowed options should be passed as identifiers.
macro_rules! unsupported_option_message {
($($option:ident,)+) => {
concat!("unsupported option. supported options: ", concat_options!($($option,)+), ".")
/// Parse a [`Flag`] from a meta.
struct ParseFlag<'a>(&'static str, &'a mut Flag);
impl<'a> MetaParse for ParseFlag<'a> {
fn name(&self) -> &'static str {
self.0
}
fn force_parse_at_meta<'x>(&mut self, meta: meta::ParseNestedMeta<'x>) -> Result<()> {
if self.1.is_set() {
return Err(Error::new_spanned(
meta.path,
format!("flag {} is already set", self.name()),
));
}
*self.1 = Flag::Present(meta.path.clone());
Ok(())
}
}
/// Parse some fields out of a [`syn::meta::ParseNestedMeta`] struct.
/// Parse any parseable value from a meta.
struct ParseValue<'a, T: parse::Parse>(&'static str, &'a mut Option<T>);
impl<'a, T: parse::Parse> MetaParse for ParseValue<'a, T> {
fn name(&self) -> &'static str {
self.0
}
fn force_parse_at_meta<'x>(&mut self, meta: meta::ParseNestedMeta<'x>) -> Result<()> {
if self.1.is_some() {
return Err(Error::new_spanned(
meta.path,
format!("duplicate {} option", self.name()),
));
}
*self.1 = Some(meta.value()?.parse()?);
Ok(())
}
}
/// Parse a `Vec<ExtractMeta>` from a meta.
struct ParseExtracts<'a>(&'a mut Vec<ExtractMeta>);
impl<'a> MetaParse for ParseExtracts<'a> {
fn name(&self) -> &'static str {
"extract"
}
fn force_parse_at_meta<'x>(&mut self, meta: meta::ParseNestedMeta<'x>) -> Result<()> {
meta.parse_nested_meta(|meta| {
self.0.push(ExtractMeta::parse_from_meta(meta)?);
Ok(())
})
}
}
/// Helper macro to chain multiple [`MetaParse`] structs in a series.
///
/// The first argument, `$meta`, must be the `ParseNestedMeta` struct. The
/// remaining arguments, except for the last, must be identifiers of arguments
/// to parse out of `$meta`.
///
/// Each `$field` is stringified and matched against `$meta.path.is_ident`
/// in the order they are given. Each `$field` must also be available as
/// mutable `Option<T>` variable outside the scope of this macro. If the
/// `$field` matches the `$meta`, the value is extracted and parsed and
/// assigned as `Some(..)` to the `$field`.
///
/// If the field has already been assigned to, an error is returned.
///
/// Lastly, if no `$field` matched the identifier of the `$meta.path`, the
/// `$else` block is evaluated. This can (and must) be used to return an error
/// or chain a call to [`parse_some_flags!`].
macro_rules! parse_some_fields {
($meta:ident, $($field:ident,)+ $else:block) => {
$(
if $meta.path.is_ident(stringify!($field)) {
if $field.is_some() {
return Err(syn::Error::new_spanned(
$meta.path,
concat!("duplicate ", stringify!($field), " option"),
));
/// This attempts to parse one `option` after the other, each of which must
/// be an expression evaluating to a thing implementing [`MetaParse`]. If any
/// matches, `Ok(())` is returned. If none matches, an error message containing
/// the allowed option names by calling `name()` on the `option` values is
/// returned.
macro_rules! parse_meta {
($meta:ident, $($option:expr,)+) => {
#[allow(unused_assignments)]
{
let meta = $meta;
$(
let meta = match $option.parse_at_meta(meta) {
Ok(ControlFlow::Continue(meta)) => meta,
Ok(ControlFlow::Break(())) => return Ok(()),
Err(e) => return Err(e),
};
)+
let mut error = format!("unsupported option. supported options are: ");
let mut first = true;
$(
error.reserve($option.name().len() + 2);
if !first {
error.push_str(", ");
}
$field = Some($meta.value()?.parse()?);
Ok(())
} else
)+
$else
}
}
/// Parse some flags out of a [`syn::meta::ParseNestedMeta`] struct.
///
/// The first argument, `$meta`, must be the `ParseNestedMeta` struct. The
/// remaining arguments, except for the last, must be identifiers of arguments
/// to parse out of `$meta`.
///
/// Each `$flag` is stringified and matched against `$meta.path.is_ident`
/// in the order they are given. Each `$flag` must also be available as
/// mutable [`Flag`] variable outside the scope of this macro. If the
/// `$flag` matches the `$meta`, the `$flag` is assigned `Flag::Present` with
/// the path of the meta as value.
///
/// If the flag has already been set, an error is returned.
///
/// Lastly, if no `$flag` matched the identifier of the `$meta.path`, the
/// `$else` block is evaluated. This can (and must) be used to return an error
/// or chain a call to [`parse_some_fields!`].
macro_rules! parse_some_flags {
($meta:ident, $($flag:ident,)+ $else:block) => {
$(
if $meta.path.is_ident(stringify!($flag)) {
if $flag.is_set() {
return Err(syn::Error::new_spanned(
$meta.path,
concat!("duplicate ", stringify!($flag), " flag"),
));
}
$flag = Flag::Present($meta.path);
Ok(())
} else
)+
$else
first = false;
error.push_str($option.name());
)+
Err(Error::new_spanned(meta.path, error))
}
}
}
@ -325,21 +354,16 @@ impl XmlCompoundMeta {
let mut prepare: Option<Path> = None;
attr.parse_nested_meta(|meta| {
parse_some_fields!(meta, name, namespace, validate, prepare, {
parse_some_flags!(meta, fallback, transparent, exhaustive, {
Err(syn::Error::new_spanned(
meta.path,
unsupported_option_message!(
name,
namespace,
validate,
fallback,
transparent,
exhaustive,
),
))
})
})
parse_meta!(
meta,
ParseValue("name", &mut name),
ParseValue("namespace", &mut namespace),
ParseFlag("fallback", &mut fallback),
ParseFlag("transparent", &mut transparent),
ParseFlag("exhaustive", &mut exhaustive),
ParseValue("validate", &mut validate),
ParseValue("prepare", &mut prepare),
)
})?;
Ok(Self {
@ -440,35 +464,13 @@ impl AttributeMeta {
let mut ty: Option<Type> = None;
let mut default_on_missing: Flag = Flag::Absent;
meta.parse_nested_meta(|meta| {
parse_some_fields!(meta, namespace, name, {
if meta.path.is_ident("type") {
if ty.is_some() {
return Err(syn::Error::new_spanned(
meta.path,
"duplicate type option",
));
}
ty = Some(meta.value()?.parse()?);
Ok(())
} else if meta.path.is_ident("default") {
if default_on_missing.is_set() {
return Err(syn::Error::new_spanned(
meta.path,
"duplicate default option",
));
}
default_on_missing = Flag::Present(meta.path);
Ok(())
} else {
Err(Error::new_spanned(
meta.path,
format!(
"unsupported option. supported options are: {}",
concat_options!(namespace, name, default, type,),
),
))
}
})
parse_meta!(
meta,
ParseValue("name", &mut name),
ParseValue("namespace", &mut namespace),
ParseValue("type", &mut ty),
ParseFlag("default", &mut default_on_missing),
)
})?;
Ok(Self {
namespace,
@ -530,25 +532,9 @@ impl ExtractMeta {
if meta.path.is_ident("text") {
if meta.input.peek(token::Paren) {
let mut ty: Option<Type> = None;
#[rustfmt::skip] // rustfmt transforms the code so that the attribute inside parse_meta! is on an expression which is not allowed
meta.parse_nested_meta(|meta| {
if meta.path.is_ident("type") {
if ty.is_some() {
return Err(syn::Error::new_spanned(
meta.path,
"duplicate type option",
));
}
ty = Some(meta.value()?.parse()?);
Ok(())
} else {
Err(Error::new_spanned(
meta.path,
format!(
"unsupported option. supported options are: {}",
concat_options!(type,),
),
))
}
parse_meta!(meta, ParseValue("type", &mut ty),)
})?;
Ok(Self::Text { ty })
} else {
@ -735,29 +721,13 @@ impl XmlFieldMeta {
let mut extract: Vec<ExtractMeta> = Vec::new();
let mut default_on_missing = Flag::Absent;
meta.parse_nested_meta(|meta| {
parse_some_fields!(meta, namespace, name, {
if meta.path.is_ident("extract") {
meta.parse_nested_meta(|meta| {
extract.push(ExtractMeta::parse_from_meta(meta)?);
Ok(())
})?;
Ok(())
} else if meta.path.is_ident("default") {
if default_on_missing.is_set() {
return Err(syn::Error::new_spanned(
meta.path,
"duplicate default option",
));
}
default_on_missing = Flag::Present(meta.path);
Ok(())
} else {
Err(Error::new_spanned(
meta.path,
unsupported_option_message!(namesace, name, extract,),
))
}
})
parse_meta!(
meta,
ParseValue("name", &mut name),
ParseValue("namespace", &mut namespace),
ParseExtracts(&mut extract),
ParseFlag("default", &mut default_on_missing),
)
})?;
Ok((namespace, name, extract, default_on_missing))
} else {