Skip to content

Commit

Permalink
Derive stringly and numeric ToVariant for fieldless enums
Browse files Browse the repository at this point in the history
Adds the `#[variant(enum = "repr")]` and `#[variant(enum = "str")]`
item-level attribute to the Variant conversion derive macros for
fieldless enums.

- `#[variant(enum = "repr")]` - convert through the numeric type
  specified using `#[repr]`.
- `#[variant(enum = "str")]` - convert through string
  representations of
  the variant names.

Close #546.
  • Loading branch information
chitoyuu committed Oct 19, 2022
1 parent 553bf87 commit 63fcc2e
Show file tree
Hide file tree
Showing 19 changed files with 1,113 additions and 391 deletions.
4 changes: 2 additions & 2 deletions gdnative-derive/src/native_script/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,14 +430,14 @@ mod tests {
assert!(
derived.is_ok(),
"Valid derive expression fails to compile:\n{}",
input().to_string()
input()
);
} else {
assert_eq!(
derived.unwrap_err().to_string(),
"The `#[property]` attribute requires explicit paths for `get` and `set` argument; \
the defaults #[property], #[property(get)] and #[property(set)] are not allowed.",
"Invalid derive expression compiles by mistake:\n{}", input().to_string()
"Invalid derive expression compiles by mistake:\n{}", input()
);
}
}
Expand Down
321 changes: 74 additions & 247 deletions gdnative-derive/src/variant/attr.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,75 @@
use std::iter::FromIterator;

use proc_macro2::Span;
use syn::spanned::Spanned;

use super::Direction;
macro_rules! impl_options {
{
self: $self:ident,
match $ident:ident . as_str() {
$( $name:ident, )*
$( $alias:literal => $name_aliased:ident, )*
}
} => (
match $ident.as_str() {
$(
stringify!($name) => {
$self.$name = true;
return Ok(());
},
)*
$(
$alias => {
$self.$name_aliased = true;
return Ok(());
},
)*
_ => {},
}
);
{
self: $self:ident,
match $ident:ident . as_str() = $lit:ident {
$( $name:ident: $ty:ty, )*
$( $alias:literal => $name_aliased:ident: $ty_aliased:ty, )*
}
} => (
match $ident.as_str() {
$(
stringify!($name) => {
let val = match $lit {
syn::Lit::Str(lit_str) => lit_str.parse::<$ty>()?,
_ => return Err(syn::Error::new($lit.span(), "expected string literal")),
};

if $self.$name.replace(val).is_some() {
return Err(syn::Error::new($lit.span(), format!(
"the argument {} is already set",
stringify!($name),
)));
}

#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Attr {
pub skip_to_variant: bool,
pub skip_from_variant: bool,
pub to_variant_with: Option<syn::Path>,
pub from_variant_with: Option<syn::Path>,
}
return Ok(());
},
)*
$(
$alias => {
let val = match $lit {
syn::Lit::Str(lit_str) => lit_str.parse::<$ty_aliased>()?,
_ => return Err(syn::Error::new($lit.span(), "expected string literal")),
};

if $self.$name_aliased.replace(val).is_some() {
return Err(syn::Error::new($lit.span(), format!(
"the argument {} is already set",
$alias,
)));
}

impl Attr {
pub(crate) fn skip_bounds(&self, dir: Direction) -> bool {
match dir {
Direction::To => self.skip_to_variant,
Direction::From => self.skip_from_variant,
return Ok(());
},
)*
_ => {},
}
}
}

#[derive(Debug, Default)]
pub struct AttrBuilder {
skip_to_variant: bool,
skip_from_variant: bool,
to_variant_with: Option<syn::Path>,
from_variant_with: Option<syn::Path>,
errors: Vec<syn::Error>,
)
}

fn generate_error_with_docs(span: Span, message: &str) -> syn::Error {
fn generate_error_with_docs(span: proc_macro2::Span, message: &str) -> syn::Error {
syn::Error::new(
span,
format!(
Expand All @@ -41,224 +79,13 @@ fn generate_error_with_docs(span: Span, message: &str) -> syn::Error {
)
}

impl AttrBuilder {
fn extend_meta(&mut self, meta: &syn::Meta) {
match meta {
syn::Meta::Path(flag) => self.set_flag(flag),
syn::Meta::NameValue(pair) => self.set_pair(pair),
syn::Meta::List(list) => {
for nested in list.nested.iter() {
match nested {
syn::NestedMeta::Meta(meta) => self.extend_meta(meta),
_ => {
self.errors
.push(syn::Error::new(nested.span(), "unexpected nested meta"));
}
}
}
}
}
}

fn set_flag(&mut self, flag: &syn::Path) {
let err = self.try_set_flag(flag).err();
self.errors.extend(err);
}

fn try_set_flag(&mut self, flag: &syn::Path) -> Result<(), syn::Error> {
let name = flag
.get_ident()
.ok_or_else(|| generate_error_with_docs(flag.span(), "Invalid syntax"))?
.to_string();

macro_rules! impl_options {
{
match $ident:ident . as_str() {
$( $name:ident, )*
}
} => (
match $ident.as_str() {
$(
stringify!($name) => {
self.$name = true;
return Ok(());
},
)*
_ => {},
}
)
}

impl_options! {
match name.as_str() {
skip_to_variant,
skip_from_variant,
}
}

#[allow(clippy::single_match)]
match name.as_str() {
"skip" => {
self.skip_to_variant = true;
self.skip_from_variant = true;
return Ok(());
}
_ => {}
}

Err(generate_error_with_docs(
flag.span(),
"Missing macro arguments",
))
}

fn set_pair(&mut self, pair: &syn::MetaNameValue) {
let err = self.try_set_pair(pair).err();
self.errors.extend(err);
}

#[allow(clippy::single_match)]
fn try_set_pair(&mut self, pair: &syn::MetaNameValue) -> Result<(), syn::Error> {
let syn::MetaNameValue { path, lit, .. } = pair;

const VALID_KEYS: &str =
"to_variant_with, from_variant_with, with, skip_to_variant, skip_from_variant, skip";

let name = path
.get_ident()
.ok_or_else(|| {
let path_token = path.segments.iter().enumerate().fold(
String::new(),
|mut paths, (index, segment)| {
if index > 0 {
paths.push_str("::");
}
paths.push_str(&segment.ident.to_string());
paths
},
);
syn::Error::new(
path.span(),
&format!("Found {}, expected one of:\n\t{}", path_token, VALID_KEYS),
)
})?
.to_string();

macro_rules! impl_options {
{
match $ident:ident . as_str() = $lit:ident {
$( $name:ident: $ty:ty, )*
}
} => (
match $ident.as_str() {
$(
stringify!($name) => {
let val = match $lit {
syn::Lit::Str(lit_str) => lit_str.parse::<$ty>()?,
_ => return Err(syn::Error::new($lit.span(), "expected string literal")),
};

if self.$name.replace(val).is_some() {
return Err(syn::Error::new($lit.span(), format!(
"the argument {} is already set",
stringify!($name),
)));
}

return Ok(());
},
)*
_ => {},
}
)
}

impl_options! {
match name.as_str() = lit {
to_variant_with: syn::Path,
from_variant_with: syn::Path,
}
}

match name.as_str() {
"with" => {
let path = match lit {
syn::Lit::Str(lit_str) => lit_str.parse::<syn::Path>()?,
_ => {
return Err(syn::Error::new(
lit.span(),
"expecting a path to a module in double quotes: #[variant(with = \"path::to::mod\")]",
))
}
};

if self
.to_variant_with
.replace(parse_quote!(#path::to_variant))
.is_some()
{
return Err(syn::Error::new(
lit.span(),
"the argument to_variant_with is already set",
));
}

if self
.from_variant_with
.replace(parse_quote!(#path::from_variant))
.is_some()
{
return Err(syn::Error::new(
lit.span(),
"the argument from_variant_with is already set",
));
}

return Ok(());
}
_ => {}
}

Err(syn::Error::new(
path.span(),
format!("unknown argument, expected one of:\n\t{}", VALID_KEYS),
))
}
pub trait AttrBuilder: FromIterator<syn::Meta> {
type Attr;
fn done(self) -> Result<Self::Attr, syn::Error>;
}

impl FromIterator<syn::Meta> for AttrBuilder {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = syn::Meta>,
{
let mut builder = AttrBuilder::default();
for meta in iter {
builder.extend_meta(&meta);
}
builder
}
}

impl AttrBuilder {
pub fn done(mut self) -> Result<Attr, syn::Error> {
if self.errors.is_empty() {
Ok(Attr {
skip_to_variant: self.skip_to_variant,
skip_from_variant: self.skip_from_variant,
to_variant_with: self.to_variant_with,
from_variant_with: self.from_variant_with,
})
} else {
let first_error = self.errors.remove(0);
let errors = self
.errors
.into_iter()
.fold(first_error, |mut errors, error| {
errors.combine(error);
errors
});
pub mod field;
pub mod item;

Err(errors)
}
}
}
pub use field::{FieldAttr, FieldAttrBuilder};
pub use item::{ItemAttr, ItemAttrBuilder};
Loading

0 comments on commit 63fcc2e

Please sign in to comment.