diff --git a/gdnative-derive/src/native_script/mod.rs b/gdnative-derive/src/native_script/mod.rs index fc8b670d4..046586fb3 100644 --- a/gdnative-derive/src/native_script/mod.rs +++ b/gdnative-derive/src/native_script/mod.rs @@ -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() ); } } diff --git a/gdnative-derive/src/variant/attr.rs b/gdnative-derive/src/variant/attr.rs index e8db24eb3..4120a5b3f 100644 --- a/gdnative-derive/src/variant/attr.rs +++ b/gdnative-derive/src/variant/attr.rs @@ -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, - pub from_variant_with: Option, -} + 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, - from_variant_with: Option, - errors: Vec, + ) } -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!( @@ -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::()?, - _ => { - 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 { + type Attr; + fn done(self) -> Result; } -impl FromIterator for AttrBuilder { - fn from_iter(iter: I) -> Self - where - I: IntoIterator, - { - let mut builder = AttrBuilder::default(); - for meta in iter { - builder.extend_meta(&meta); - } - builder - } -} - -impl AttrBuilder { - pub fn done(mut self) -> Result { - 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}; diff --git a/gdnative-derive/src/variant/attr/field.rs b/gdnative-derive/src/variant/attr/field.rs new file mode 100644 index 000000000..aad491fb8 --- /dev/null +++ b/gdnative-derive/src/variant/attr/field.rs @@ -0,0 +1,222 @@ +use std::iter::FromIterator; + +use proc_macro2::Span; +use syn::spanned::Spanned; + +use crate::variant::Direction; + +use super::AttrBuilder; + +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct FieldAttr { + pub skip_to_variant: bool, + pub skip_from_variant: bool, + pub to_variant_with: Option, + pub from_variant_with: Option, +} + +impl FieldAttr { + pub(crate) fn skip_bounds(&self, dir: Direction) -> bool { + match dir { + Direction::To => self.skip_to_variant, + Direction::From => self.skip_from_variant, + } + } +} + +#[derive(Debug, Default)] +pub struct FieldAttrBuilder { + skip_to_variant: bool, + skip_from_variant: bool, + to_variant_with: Option, + from_variant_with: Option, + errors: Vec, +} + +fn generate_error_with_docs(span: Span, message: &str) -> syn::Error { + syn::Error::new( + span, + format!( + "{}\n\texpecting #[variant(...)]. See documentation:\n\thttps://docs.rs/gdnative/0.9.0/gdnative/core_types/trait.ToVariant.html#field-attributes", + message + ), + ) +} + +impl FieldAttrBuilder { + 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(); + + impl_options! { + self: self, + 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(); + + impl_options! { + self: self, + 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::()?, + _ => { + 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), + )) + } +} + +impl FromIterator for FieldAttrBuilder { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + let mut builder = FieldAttrBuilder::default(); + for meta in iter { + builder.extend_meta(&meta); + } + builder + } +} + +impl AttrBuilder for FieldAttrBuilder { + type Attr = FieldAttr; + fn done(mut self) -> Result { + if self.errors.is_empty() { + Ok(FieldAttr { + 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 + }); + + Err(errors) + } + } +} diff --git a/gdnative-derive/src/variant/attr/item.rs b/gdnative-derive/src/variant/attr/item.rs new file mode 100644 index 000000000..070d2b254 --- /dev/null +++ b/gdnative-derive/src/variant/attr/item.rs @@ -0,0 +1,141 @@ +use std::iter::FromIterator; + +use proc_macro2::Span; +use syn::spanned::Spanned; + +use crate::variant::{attr::generate_error_with_docs, repr::EnumReprKind}; + +use super::AttrBuilder; + +#[derive(Clone, Debug)] +pub struct ItemAttr { + pub enum_repr_kind: Option<(EnumReprKind, Span)>, +} + +#[derive(Debug, Default)] +pub struct ItemAttrBuilder { + enum_repr_kind: Option, + + errors: Vec, +} + +impl ItemAttrBuilder { + 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> { + Err(generate_error_with_docs( + flag.span(), + "Unknown flag, or 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 = "enum"; + + 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(); + + impl_options! { + self: self, + match name.as_str() = lit { + "enum" => enum_repr_kind: syn::Ident, + } + } + + Err(syn::Error::new( + path.span(), + format!("unknown argument, expected one of:\n\t{}", VALID_KEYS), + )) + } +} + +impl FromIterator for ItemAttrBuilder { + fn from_iter(iter: I) -> Self + where + I: IntoIterator, + { + let mut builder = ItemAttrBuilder::default(); + for meta in iter { + builder.extend_meta(&meta); + } + builder + } +} + +impl AttrBuilder for ItemAttrBuilder { + type Attr = ItemAttr; + fn done(mut self) -> Result { + if self.errors.is_empty() { + let enum_repr_kind = self + .enum_repr_kind + .map(|kind| match &*kind.to_string() { + "repr" => Ok((EnumReprKind::Repr, kind.span())), + "str" => Ok((EnumReprKind::Str, kind.span())), + _ => Err(syn::Error::new( + kind.span(), + "unknown enum representation, expected values: repr, str", + )), + }) + .transpose()?; + + Ok(ItemAttr { enum_repr_kind }) + } else { + let first_error = self.errors.remove(0); + let errors = self + .errors + .into_iter() + .fold(first_error, |mut errors, error| { + errors.combine(error); + errors + }); + + Err(errors) + } + } +} diff --git a/gdnative-derive/src/variant/bounds.rs b/gdnative-derive/src/variant/bounds.rs index 0907e40db..968d48f21 100644 --- a/gdnative-derive/src/variant/bounds.rs +++ b/gdnative-derive/src/variant/bounds.rs @@ -3,8 +3,9 @@ use syn::visit::Visit; use syn::{GenericParam, Generics}; use crate::extend_bounds::{with_visitor, BoundsVisitor}; +use crate::variant::repr::StructRepr; -use super::repr::{Field, Repr, VariantRepr}; +use super::repr::{EnumRepr, Field, Repr, VariantRepr}; use super::Direction; pub(crate) fn extend_bounds( @@ -21,7 +22,7 @@ pub(crate) fn extend_bounds( dir: Direction, ) { match repr { - VariantRepr::Unit => {} + VariantRepr::Unit(_) => {} VariantRepr::Tuple(tys) => { for Field { ty, attr, .. } in tys.iter() { if !attr.skip_bounds(dir) { @@ -40,12 +41,12 @@ pub(crate) fn extend_bounds( } match repr { - Repr::Enum(ref variants) => { + Repr::Enum(EnumRepr { ref variants, .. }) => { for (_, var_repr) in variants.iter() { visit_var_repr(visitor, var_repr, dir); } } - Repr::Struct(var_repr) => { + Repr::Struct(StructRepr(var_repr)) => { visit_var_repr(visitor, var_repr, dir); } } diff --git a/gdnative-derive/src/variant/from.rs b/gdnative-derive/src/variant/from.rs index 1186b9425..3cd369409 100644 --- a/gdnative-derive/src/variant/from.rs +++ b/gdnative-derive/src/variant/from.rs @@ -1,9 +1,10 @@ use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; use crate::variant::bounds; +use crate::variant::repr::VariantRepr; use syn::Ident; -use super::repr::Repr; +use super::repr::{EnumRepr, EnumReprKind, Repr, StructRepr}; use super::DeriveData; pub(crate) fn expand_from_variant(derive_data: DeriveData) -> Result { @@ -22,7 +23,7 @@ pub(crate) fn expand_from_variant(derive_data: DeriveData) -> Result { + Repr::Struct(StructRepr(var_repr)) => { let from_variant = var_repr.make_from_variant_expr(&input_ident, "e! { #ident })?; quote! { { @@ -30,83 +31,110 @@ pub(crate) fn expand_from_variant(derive_data: DeriveData) -> Result { - let var_input_ident = Ident::new("__enum_variant", Span::call_site()); + Repr::Enum(EnumRepr { + variants, + kind, + primitive_repr, + }) => match kind { + EnumReprKind::External => expand_external(&ident, &input_ident, variants)?, + EnumReprKind::Str => { + if let Some((var_ident, _)) = variants + .iter() + .find(|(_, var_repr)| !matches!(var_repr, VariantRepr::Unit(_))) + { + return Err(syn::Error::new( + var_ident.span(), + "`str` representation can only be used for fieldless enums", + )); + } - let var_ident_strings: Vec = variants - .iter() - .map(|(var_ident, _)| format!("{}", var_ident)) - .collect(); + let var_ident_strings: Vec = variants + .iter() + .map(|(var_ident, _)| format!("{}", var_ident)) + .collect(); - let var_ident_string_literals = var_ident_strings - .iter() - .map(|string| Literal::string(string)) - .collect::>(); + let var_ident_string_literals = var_ident_strings + .iter() + .map(|string| Literal::string(string)) + .collect::>(); - let ref_var_ident_string_literals = &var_ident_string_literals; + let ref_var_ident_string_literals = &var_ident_string_literals; - let var_from_variants = variants - .iter() - .map(|(var_ident, var_repr)| { - var_repr - .make_from_variant_expr(&var_input_ident, "e! { #ident::#var_ident }) - }) - .collect::, _>>()?; + let variant_idents = variants.iter().map(|(var_ident, _)| var_ident); - let var_input_ident_iter = std::iter::repeat(&var_input_ident); + let early_return = variants.is_empty().then(|| { + quote! { + return Err(FVE::UnknownEnumVariant { + variant: __variant, + expected: &[], + }); + } + }); - // Return `FromVariantError` if input is an uninhabitable enum - let early_return = variants.is_empty().then(|| { quote! { - return Err(FVE::UnknownEnumVariant { - variant: __key, - expected: &[], - }); + let __variant = String::from_variant(#input_ident)?; + + #early_return + + match __variant.as_str() { + #( + #ref_var_ident_string_literals => { + Ok(#ident::#variant_idents) + }, + )* + variant => Err(FVE::UnknownEnumVariant { + variant: variant.to_string(), + expected: &[#(#ref_var_ident_string_literals),*], + }), + } } - }); + } + EnumReprKind::Repr => { + let primitive_repr = primitive_repr.ok_or_else(|| { + syn::Error::new( + ident.span(), + "a primitive representation must be specified using `#[repr]`", + ) + })?; - quote! { - let __dict = ::gdnative::core_types::Dictionary::from_variant(#input_ident) - .map_err(|__err| FVE::InvalidEnumRepr { - expected: VariantEnumRepr::ExternallyTagged, - error: std::boxed::Box::new(__err), - })?; - let __keys = __dict.keys(); - if __keys.len() != 1 { - return Err(FVE::InvalidEnumRepr { - expected: VariantEnumRepr::ExternallyTagged, - error: std::boxed::Box::new(FVE::InvalidLength { - expected: 1, - len: __keys.len() as usize, - }), - }) + let mut clauses = Vec::new(); + let mut hints = Vec::new(); + let mut discriminant = quote! { 0 }; + for (var_ident, var_repr) in variants.iter() { + if let VariantRepr::Unit(expr) = var_repr { + if let Some(expr) = expr { + discriminant = quote!(#expr); + } + } else { + return Err(syn::Error::new( + var_ident.span(), + "`repr` representation can only be used for fieldless enums", + )); + } + + clauses.push(quote! { + if __value == (#discriminant) { + return Ok(#ident::#var_ident); + } + }); + + hints.push(Literal::string(&format!("{}({})", var_ident, discriminant))); + + discriminant = quote!(1 + (#discriminant)); } - let __key = String::from_variant(&__keys.get(0)) - .map_err(|__err| FVE::InvalidEnumRepr { - expected: VariantEnumRepr::ExternallyTagged, - error: std::boxed::Box::new(__err), - })?; + quote! { + let __value = <#primitive_repr>::from_variant(#input_ident)?; - #early_return - - match __key.as_str() { - #( - #ref_var_ident_string_literals => { - let #var_input_ident_iter = &__dict.get_or_nil(&__keys.get(0)); - (#var_from_variants).map_err(|err| FVE::InvalidEnumVariant { - variant: #ref_var_ident_string_literals, - error: std::boxed::Box::new(err), - }) - }, - )* - variant => Err(FVE::UnknownEnumVariant { - variant: variant.to_string(), - expected: &[#(#ref_var_ident_string_literals),*], - }), + #(#clauses)* + + Err(FVE::UnknownEnumVariant { + variant: format!("({})", __value), + expected: &[#(#hints),*], + }) } } - } + }, }; let generics_no_bounds = bounds::remove_bounds(generics.clone()); @@ -131,3 +159,82 @@ pub(crate) fn expand_from_variant(derive_data: DeriveData) -> Result, +) -> Result { + let var_input_ident = Ident::new("__enum_variant", Span::call_site()); + + let var_ident_strings: Vec = variants + .iter() + .map(|(var_ident, _)| format!("{}", var_ident)) + .collect(); + + let var_ident_string_literals = var_ident_strings + .iter() + .map(|string| Literal::string(string)) + .collect::>(); + + let ref_var_ident_string_literals = &var_ident_string_literals; + + let var_from_variants = variants + .iter() + .map(|(var_ident, var_repr)| { + var_repr.make_from_variant_expr(&var_input_ident, "e! { #ident::#var_ident }) + }) + .collect::, _>>()?; + + let var_input_ident_iter = std::iter::repeat(&var_input_ident); + let early_return = variants.is_empty().then(|| { + quote! { + return Err(FVE::UnknownEnumVariant { + variant: __key, + expected: &[], + }); + } + }); + + Ok(quote! { + let __dict = ::gdnative::core_types::Dictionary::from_variant(#input_ident) + .map_err(|__err| FVE::InvalidEnumRepr { + expected: VariantEnumRepr::ExternallyTagged, + error: std::boxed::Box::new(__err), + })?; + let __keys = __dict.keys(); + if __keys.len() != 1 { + return Err(FVE::InvalidEnumRepr { + expected: VariantEnumRepr::ExternallyTagged, + error: std::boxed::Box::new(FVE::InvalidLength { + expected: 1, + len: __keys.len() as usize, + }), + }) + } + + let __key = String::from_variant(&__keys.get(0)) + .map_err(|__err| FVE::InvalidEnumRepr { + expected: VariantEnumRepr::ExternallyTagged, + error: std::boxed::Box::new(__err), + })?; + + #early_return + + match __key.as_str() { + #( + #ref_var_ident_string_literals => { + let #var_input_ident_iter = &__dict.get_or_nil(&__keys.get(0)); + (#var_from_variants).map_err(|err| FVE::InvalidEnumVariant { + variant: #ref_var_ident_string_literals, + error: std::boxed::Box::new(err), + }) + }, + )* + variant => Err(FVE::UnknownEnumVariant { + variant: variant.to_string(), + expected: &[#(#ref_var_ident_string_literals),*], + }), + } + }) +} diff --git a/gdnative-derive/src/variant/mod.rs b/gdnative-derive/src/variant/mod.rs index 0f56e3048..2dc9d7202 100644 --- a/gdnative-derive/src/variant/mod.rs +++ b/gdnative-derive/src/variant/mod.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream as TokenStream2; -use syn::{spanned::Spanned, Data, DeriveInput, Generics, Ident}; +use syn::{spanned::Spanned, Data, DeriveInput, Generics, Ident, Meta}; mod attr; mod bounds; @@ -8,7 +8,12 @@ mod repr; mod to; use bounds::extend_bounds; -use repr::{Repr, VariantRepr}; +use repr::Repr; + +use self::{ + attr::{AttrBuilder, ItemAttrBuilder}, + repr::{EnumRepr, StructRepr}, +}; pub(crate) struct DeriveData { pub(crate) ident: Ident, @@ -51,25 +56,75 @@ impl ToVariantTrait { } } +fn improve_meta_error(err: syn::Error) -> syn::Error { + let error = err.to_string(); + match error.as_str() { + "expected literal" => { + syn::Error::new(err.span(), "String expected, wrap with double quotes.") + } + other => syn::Error::new( + err.span(), + format!("{}, ie: #[variant(with = \"...\")]", other), + ), + } +} + +fn parse_attrs<'a, A, I>(attrs: I) -> Result +where + A: AttrBuilder, + I: IntoIterator, +{ + attrs + .into_iter() + .filter(|attr| attr.path.is_ident("variant")) + .map(|attr| attr.parse_meta().map_err(improve_meta_error)) + .collect::>()? + .done() +} + pub(crate) fn parse_derive_input( input: DeriveInput, bound: &syn::Path, dir: Direction, ) -> Result { + let item_attr = parse_attrs::(&input.attrs)?; + let repr = match input.data { - Data::Struct(struct_data) => Repr::Struct(VariantRepr::repr_for(&struct_data.fields)?), - Data::Enum(enum_data) => Repr::Enum( - enum_data - .variants - .iter() - .map(|variant| { - Ok(( - variant.ident.clone(), - VariantRepr::repr_for(&variant.fields)?, - )) - }) - .collect::>()?, - ), + Data::Struct(struct_data) => { + Repr::Struct(StructRepr::repr_for(item_attr, &struct_data.fields)?) + } + Data::Enum(enum_data) => { + let primitive_repr = input.attrs.iter().find_map(|attr| { + if !attr.path.is_ident("repr") { + return None; + } + + // rustc should do the complaining for us if the `repr` attribute is invalid + if let Ok(Meta::List(list)) = attr.parse_meta() { + list.nested.iter().find_map(|meta| { + if let syn::NestedMeta::Meta(Meta::Path(p)) = meta { + p.get_ident() + .map_or(false, |ident| { + let ident = ident.to_string(); + ident.starts_with('u') || ident.starts_with('i') + }) + .then(|| { + syn::Type::Path(syn::TypePath { + qself: None, + path: p.clone(), + }) + }) + } else { + None + } + }) + } else { + None + } + }); + + Repr::Enum(EnumRepr::repr_for(item_attr, primitive_repr, &enum_data)?) + } Data::Union(_) => { return Err(syn::Error::new( input.span(), diff --git a/gdnative-derive/src/variant/repr.rs b/gdnative-derive/src/variant/repr.rs index babbc4120..32c1d41b0 100644 --- a/gdnative-derive/src/variant/repr.rs +++ b/gdnative-derive/src/variant/repr.rs @@ -1,18 +1,22 @@ use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; -use syn::{Fields, Ident, Type}; +use syn::{DataEnum, Fields, Ident, Type}; -use super::attr::{Attr, AttrBuilder}; -use super::ToVariantTrait; +use super::attr::{FieldAttr, FieldAttrBuilder, ItemAttr}; +use super::{parse_attrs, ToVariantTrait}; +// Shouldn't matter since this is immediately unpacked anyway. +// Boxing would add too much noise to the match statements. +#[allow(clippy::large_enum_variant)] #[derive(Clone, Eq, PartialEq, Debug)] pub(crate) enum Repr { - Struct(VariantRepr), - Enum(Vec<(Ident, VariantRepr)>), + Struct(StructRepr), + Enum(EnumRepr), } +#[allow(clippy::large_enum_variant)] #[derive(Clone, Eq, PartialEq, Debug)] pub(crate) enum VariantRepr { - Unit, + Unit(Option), Struct(Vec), Tuple(Vec), } @@ -21,32 +25,71 @@ pub(crate) enum VariantRepr { pub(crate) struct Field { pub ident: Ident, pub ty: Type, - pub attr: Attr, + pub attr: FieldAttr, } -fn improve_meta_error(err: syn::Error) -> syn::Error { - let error = err.to_string(); - match error.as_str() { - "expected literal" => { - syn::Error::new(err.span(), "String expected, wrap with double quotes.") - } - other => syn::Error::new( - err.span(), - format!("{}, ie: #[variant(with = \"...\")]", other), - ), +#[derive(Clone, Eq, PartialEq, Debug)] +pub(crate) struct StructRepr(pub VariantRepr); + +#[derive(Clone, Eq, PartialEq, Debug)] +pub(crate) struct EnumRepr { + pub kind: EnumReprKind, + pub primitive_repr: Option, + pub variants: Vec<(Ident, VariantRepr)>, +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum EnumReprKind { + /// Externally-tagged objects, i.e. the original behavior. + External, + /// The integer type specified by the `repr` attribute of the enum. + Repr, + /// Represent as strings. + Str, +} + +impl EnumRepr { + pub(crate) fn repr_for( + attr: ItemAttr, + primitive_repr: Option, + enum_data: &DataEnum, + ) -> Result { + let variants = enum_data + .variants + .iter() + .map(|variant| { + let mut repr = VariantRepr::repr_for(&variant.fields)?; + if let VariantRepr::Unit(discriminant) = &mut repr { + if let Some((_, expr)) = &variant.discriminant { + *discriminant = Some(expr.clone()); + } + } + + Ok((variant.ident.clone(), repr)) + }) + .collect::>()?; + + Ok(EnumRepr { + kind: attr + .enum_repr_kind + .map_or(EnumReprKind::External, |(kind, _)| kind), + primitive_repr, + variants, + }) } } -fn parse_attrs<'a, I>(attrs: I) -> Result -where - I: IntoIterator, -{ - attrs - .into_iter() - .filter(|attr| attr.path.is_ident("variant")) - .map(|attr| attr.parse_meta().map_err(improve_meta_error)) - .collect::>()? - .done() +impl StructRepr { + pub(crate) fn repr_for(attr: ItemAttr, fields: &Fields) -> Result { + if let Some((_, span)) = attr.enum_repr_kind { + return Err(syn::Error::new( + span, + "`enum` representation can only be set for enums", + )); + } + + VariantRepr::repr_for(fields).map(StructRepr) + } } impl VariantRepr { @@ -59,7 +102,7 @@ impl VariantRepr { .map(|f| { let ident = f.ident.clone().expect("fields should be named"); let ty = f.ty.clone(); - let attr = parse_attrs(&f.attrs)?; + let attr = parse_attrs::(&f.attrs)?; Ok(Field { ident, ty, attr }) }) .collect::, syn::Error>>()?, @@ -72,12 +115,12 @@ impl VariantRepr { .map(|(n, f)| { let ident = Ident::new(&format!("__field_{}", n), Span::call_site()); let ty = f.ty.clone(); - let attr = parse_attrs(&f.attrs)?; + let attr = parse_attrs::(&f.attrs)?; Ok(Field { ident, ty, attr }) }) .collect::>()?, ), - Fields::Unit => VariantRepr::Unit, + Fields::Unit => VariantRepr::Unit(None), }; Ok(this) @@ -85,7 +128,7 @@ impl VariantRepr { pub(crate) fn destructure_pattern(&self) -> TokenStream2 { match self { - VariantRepr::Unit => quote! {}, + VariantRepr::Unit(_) => quote! {}, VariantRepr::Tuple(fields) => { let names = fields.iter().map(|f| &f.ident); quote! { @@ -106,7 +149,7 @@ impl VariantRepr { trait_kind: ToVariantTrait, ) -> Result { let tokens = match self { - VariantRepr::Unit => { + VariantRepr::Unit(_) => { quote! { ::gdnative::core_types::Dictionary::new().into_shared().to_variant() } } VariantRepr::Tuple(fields) => { @@ -176,7 +219,7 @@ impl VariantRepr { ctor: &TokenStream2, ) -> Result { let tokens = match self { - VariantRepr::Unit => { + VariantRepr::Unit(_) => { quote! { if #variant.is_nil() { Err(FVE::InvalidStructRepr { diff --git a/gdnative-derive/src/variant/to.rs b/gdnative-derive/src/variant/to.rs index ae16f841f..3f656052d 100644 --- a/gdnative-derive/src/variant/to.rs +++ b/gdnative-derive/src/variant/to.rs @@ -1,7 +1,9 @@ use crate::variant::bounds; use proc_macro2::{Literal, TokenStream as TokenStream2}; -use super::repr::Repr; +use crate::variant::repr::{EnumReprKind, VariantRepr}; + +use super::repr::{EnumRepr, Repr, StructRepr}; use super::{DeriveData, ToVariantTrait}; pub(crate) fn expand_to_variant( @@ -24,7 +26,7 @@ pub(crate) fn expand_to_variant( } let return_expr = match repr { - Repr::Struct(var_repr) => { + Repr::Struct(StructRepr(var_repr)) => { let destructure_pattern = var_repr.destructure_pattern(); let to_variant = var_repr.make_to_variant_expr(trait_kind)?; quote! { @@ -34,36 +36,102 @@ pub(crate) fn expand_to_variant( } } } - Repr::Enum(variants) => { + Repr::Enum(EnumRepr { + variants, + primitive_repr, + kind, + }) => { if variants.is_empty() { quote! { unreachable!("this is an uninhabitable enum"); } } else { - let match_arms = variants - .iter() - .map(|(var_ident, var_repr)| { - let destructure_pattern = var_repr.destructure_pattern(); - let to_variant = var_repr.make_to_variant_expr(trait_kind)?; - let var_ident_string = format!("{}", var_ident); - let var_ident_string_literal = Literal::string(&var_ident_string); - let tokens = quote! { - #ident::#var_ident #destructure_pattern => { - let __dict = ::gdnative::core_types::Dictionary::new(); - let __key = ::gdnative::core_types::ToVariant::to_variant( - &::gdnative::core_types::GodotString::from(#var_ident_string_literal) - ); - let __value = #to_variant; - __dict.insert(&__key, &__value); - ::gdnative::core_types::ToVariant::to_variant(&__dict.into_shared()) + match kind { + EnumReprKind::External => { + let match_arms = variants + .iter() + .map(|(var_ident, var_repr)| { + let destructure_pattern = var_repr.destructure_pattern(); + let to_variant = var_repr.make_to_variant_expr(trait_kind)?; + let var_ident_string = format!("{}", var_ident); + let var_ident_string_literal = Literal::string(&var_ident_string); + let tokens = quote! { + #ident::#var_ident #destructure_pattern => { + let __dict = ::gdnative::core_types::Dictionary::new(); + let __key = ::gdnative::core_types::ToVariant::to_variant( + &::gdnative::core_types::GodotString::from(#var_ident_string_literal) + ); + let __value = #to_variant; + __dict.insert(&__key, &__value); + ::gdnative::core_types::ToVariant::to_variant(&__dict.into_shared()) + } + }; + Ok(tokens) + }) + .collect::, syn::Error>>()?; + + quote! { + match #to_variant_receiver { + #( #match_arms ),* } - }; - Ok(tokens) - }).collect::,syn::Error>>()?; + } + } + EnumReprKind::Str => { + let match_arms = variants + .iter() + .map(|(var_ident, var_repr)| { + if !matches!(var_repr, VariantRepr::Unit(_)) { + return Err(syn::Error::new(var_ident.span(), "`str` representation can only be used for fieldless enums")); + } - quote! { - match #to_variant_receiver { - #( #match_arms ),* + let var_ident_string = format!("{}", var_ident); + let var_ident_string_literal = Literal::string(&var_ident_string); + let tokens = quote! { + #ident::#var_ident => { + ::gdnative::core_types::ToVariant::to_variant(#var_ident_string_literal) + } + }; + + Ok(tokens) + }) + .collect::, syn::Error>>()?; + + quote! { + match #to_variant_receiver { + #( #match_arms ),* + } + } + } + EnumReprKind::Repr => { + let primitive_repr = primitive_repr.ok_or_else(|| { + syn::Error::new( + ident.span(), + "a primitive representation must be specified using `#[repr]`", + ) + })?; + + if let Some((var_ident, _)) = variants + .iter() + .find(|(_, var_repr)| !matches!(var_repr, VariantRepr::Unit(_))) + { + return Err(syn::Error::new( + var_ident.span(), + "`repr` representation can only be used for fieldless enums", + )); + } + + match trait_kind { + ToVariantTrait::ToVariant => { + quote! { + ::gdnative::core_types::ToVariant::to_variant(&(*self as #primitive_repr)) + } + } + ToVariantTrait::OwnedToVariant => { + quote! { + ::gdnative::core_types::ToVariant::to_variant(&(self as #primitive_repr)) + } + } + } } } } diff --git a/gdnative/tests/ui.rs b/gdnative/tests/ui.rs index 14af2f9ac..f4e922e2a 100644 --- a/gdnative/tests/ui.rs +++ b/gdnative/tests/ui.rs @@ -25,6 +25,8 @@ fn ui_tests() { t.compile_fail("tests/ui/to_variant_fail_05.rs"); t.compile_fail("tests/ui/to_variant_fail_06.rs"); t.compile_fail("tests/ui/to_variant_fail_07.rs"); + t.compile_fail("tests/ui/to_variant_fail_08.rs"); + t.compile_fail("tests/ui/to_variant_fail_09.rs"); // FromVariant t.compile_fail("tests/ui/from_variant_fail_01.rs"); @@ -33,6 +35,8 @@ fn ui_tests() { t.compile_fail("tests/ui/from_variant_fail_05.rs"); t.compile_fail("tests/ui/from_variant_fail_06.rs"); t.compile_fail("tests/ui/from_variant_fail_07.rs"); + t.compile_fail("tests/ui/from_variant_fail_08.rs"); + t.compile_fail("tests/ui/from_variant_fail_09.rs"); } // FIXME(rust/issues/54725): Full path spans are only available on nightly as of now diff --git a/gdnative/tests/ui/from_variant_fail_08.rs b/gdnative/tests/ui/from_variant_fail_08.rs new file mode 100644 index 000000000..98f0a4223 --- /dev/null +++ b/gdnative/tests/ui/from_variant_fail_08.rs @@ -0,0 +1,29 @@ +use gdnative::prelude::*; + +#[derive(FromVariant)] +// `enum` representation should only be allowed on enums +#[variant(enum = "repr")] +pub struct Foo { + bar: String, +} + +#[derive(FromVariant)] +// The `repr` representation requires an explicit type +#[variant(enum = "repr")] +pub enum Bar { + A, + B, + C, +} + +#[derive(FromVariant)] +// The `repr` representation should only be allowed for fieldless enums +#[variant(enum = "repr")] +#[repr(i32)] +pub enum Baz { + A, + B(String), + C, +} + +fn main() {} diff --git a/gdnative/tests/ui/from_variant_fail_08.stderr b/gdnative/tests/ui/from_variant_fail_08.stderr new file mode 100644 index 000000000..982774e7a --- /dev/null +++ b/gdnative/tests/ui/from_variant_fail_08.stderr @@ -0,0 +1,17 @@ +error: `enum` representation can only be set for enums + --> tests/ui/from_variant_fail_08.rs:5:18 + | +5 | #[variant(enum = "repr")] + | ^^^^^^ + +error: a primitive representation must be specified using `#[repr]` + --> tests/ui/from_variant_fail_08.rs:13:10 + | +13 | pub enum Bar { + | ^^^ + +error: `repr` representation can only be used for fieldless enums + --> tests/ui/from_variant_fail_08.rs:25:5 + | +25 | B(String), + | ^ diff --git a/gdnative/tests/ui/from_variant_fail_09.rs b/gdnative/tests/ui/from_variant_fail_09.rs new file mode 100644 index 000000000..599d3b46f --- /dev/null +++ b/gdnative/tests/ui/from_variant_fail_09.rs @@ -0,0 +1,19 @@ +use gdnative::prelude::*; + +#[derive(FromVariant)] +// `enum` representation should only be allowed on enums +#[variant(enum = "str")] +pub struct Foo { + bar: String, +} + +#[derive(FromVariant)] +// The `str` representation should only be allowed for fieldless enums +#[variant(enum = "str")] +pub enum Bar { + A, + B(String), + C, +} + +fn main() {} diff --git a/gdnative/tests/ui/from_variant_fail_09.stderr b/gdnative/tests/ui/from_variant_fail_09.stderr new file mode 100644 index 000000000..9f9ab6d7d --- /dev/null +++ b/gdnative/tests/ui/from_variant_fail_09.stderr @@ -0,0 +1,11 @@ +error: `enum` representation can only be set for enums + --> tests/ui/from_variant_fail_09.rs:5:18 + | +5 | #[variant(enum = "str")] + | ^^^^^ + +error: `str` representation can only be used for fieldless enums + --> tests/ui/from_variant_fail_09.rs:15:5 + | +15 | B(String), + | ^ diff --git a/gdnative/tests/ui/to_variant_fail_08.rs b/gdnative/tests/ui/to_variant_fail_08.rs new file mode 100644 index 000000000..9b9dd4592 --- /dev/null +++ b/gdnative/tests/ui/to_variant_fail_08.rs @@ -0,0 +1,29 @@ +use gdnative::prelude::*; + +#[derive(ToVariant)] +// `enum` representation should only be allowed on enums +#[variant(enum = "repr")] +pub struct Foo { + bar: String, +} + +#[derive(ToVariant)] +// The `repr` representation requires an explicit type +#[variant(enum = "repr")] +pub enum Bar { + A, + B, + C, +} + +#[derive(ToVariant)] +// The `repr` representation should only be allowed for fieldless enums +#[variant(enum = "repr")] +#[repr(i32)] +pub enum Baz { + A, + B(String), + C, +} + +fn main() {} diff --git a/gdnative/tests/ui/to_variant_fail_08.stderr b/gdnative/tests/ui/to_variant_fail_08.stderr new file mode 100644 index 000000000..62cfb4f06 --- /dev/null +++ b/gdnative/tests/ui/to_variant_fail_08.stderr @@ -0,0 +1,17 @@ +error: `enum` representation can only be set for enums + --> tests/ui/to_variant_fail_08.rs:5:18 + | +5 | #[variant(enum = "repr")] + | ^^^^^^ + +error: a primitive representation must be specified using `#[repr]` + --> tests/ui/to_variant_fail_08.rs:13:10 + | +13 | pub enum Bar { + | ^^^ + +error: `repr` representation can only be used for fieldless enums + --> tests/ui/to_variant_fail_08.rs:25:5 + | +25 | B(String), + | ^ diff --git a/gdnative/tests/ui/to_variant_fail_09.rs b/gdnative/tests/ui/to_variant_fail_09.rs new file mode 100644 index 000000000..0d9d03a2e --- /dev/null +++ b/gdnative/tests/ui/to_variant_fail_09.rs @@ -0,0 +1,19 @@ +use gdnative::prelude::*; + +#[derive(ToVariant)] +// `enum` representation should only be allowed on enums +#[variant(enum = "str")] +pub struct Foo { + bar: String, +} + +#[derive(ToVariant)] +// The `str` representation should only be allowed for fieldless enums +#[variant(enum = "str")] +pub enum Bar { + A, + B(String), + C, +} + +fn main() {} diff --git a/gdnative/tests/ui/to_variant_fail_09.stderr b/gdnative/tests/ui/to_variant_fail_09.stderr new file mode 100644 index 000000000..a7c1ebeee --- /dev/null +++ b/gdnative/tests/ui/to_variant_fail_09.stderr @@ -0,0 +1,11 @@ +error: `enum` representation can only be set for enums + --> tests/ui/to_variant_fail_09.rs:5:18 + | +5 | #[variant(enum = "str")] + | ^^^^^ + +error: `str` representation can only be used for fieldless enums + --> tests/ui/to_variant_fail_09.rs:15:5 + | +15 | B(String), + | ^ diff --git a/test/src/test_derive.rs b/test/src/test_derive.rs index b2ceafc05..9c623f9ed 100644 --- a/test/src/test_derive.rs +++ b/test/src/test_derive.rs @@ -9,6 +9,8 @@ pub(crate) fn run_tests() -> bool { let mut status = true; status &= test_derive_to_variant(); + status &= test_derive_to_variant_repr(); + status &= test_derive_to_variant_str(); status &= test_derive_owned_to_variant(); status &= test_derive_nativeclass(); status &= test_derive_nativeclass_without_constructor(); @@ -164,6 +166,107 @@ crate::godot_itest! { test_derive_to_variant { // ---------------------------------------------------------------------------------------------------------------------------------------------- +crate::godot_itest! { test_derive_to_variant_repr { + const ANSWER: u8 = 42; + + #[derive(Copy, Clone, Eq, PartialEq, Debug, ToVariant, FromVariant)] + #[variant(enum = "repr")] + #[repr(u8)] + enum ToVarRepr { + A = 0, + B, + C, + D = 128 - 1, + E, + F = ANSWER, + } + + #[derive(Clone, Eq, PartialEq, Debug, OwnedToVariant, FromVariant)] + #[variant(enum = "repr")] + #[repr(u8)] + enum ToVarReprOwned { + A = 0, + B, + C, + D = 128 - 1, + E, + F = ANSWER, + } + + let variant = ToVarRepr::A.to_variant(); + assert_eq!(Some(0), variant.to::()); + + let variant = ToVarRepr::B.to_variant(); + assert_eq!(Some(1), variant.to::()); + + let variant = ToVarRepr::E.to_variant(); + assert_eq!(Some(128), variant.to::()); + + let variant = ToVarReprOwned::A.owned_to_variant(); + assert_eq!(Some(0), variant.to::()); + + let variant = ToVarReprOwned::C.owned_to_variant(); + assert_eq!(Some(2), variant.to::()); + + let variant = ToVarReprOwned::F.owned_to_variant(); + assert_eq!(Some(42), variant.to::()); + + assert_eq!(Some(ToVarRepr::A), Variant::new(0).to::()); + assert_eq!(Some(ToVarRepr::B), Variant::new(1).to::()); + assert_eq!(Some(ToVarRepr::C), Variant::new(2).to::()); + assert_eq!(Some(ToVarRepr::D), Variant::new(127).to::()); + assert_eq!(Some(ToVarRepr::E), Variant::new(128).to::()); + assert_eq!(Some(ToVarRepr::F), Variant::new(42).to::()); + assert_eq!(None, Variant::new(48).to::()); + assert_eq!(None, Variant::new(192).to::()); +}} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + +crate::godot_itest! { test_derive_to_variant_str { + #[derive(Clone, Eq, PartialEq, Debug, ToVariant, FromVariant)] + #[variant(enum = "str")] + enum ToVarStr { + A, + B, + C, + } + + #[derive(Clone, Eq, PartialEq, Debug, OwnedToVariant, FromVariant)] + #[variant(enum = "str")] + enum ToVarStrOwned { + A, + B, + C, + } + + let variant = ToVarStr::A.to_variant(); + assert_eq!(Some("A"), variant.to::().as_deref()); + + let variant = ToVarStr::B.to_variant(); + assert_eq!(Some("B"), variant.to::().as_deref()); + + let variant = ToVarStr::C.to_variant(); + assert_eq!(Some("C"), variant.to::().as_deref()); + + let variant = ToVarStrOwned::A.owned_to_variant(); + assert_eq!(Some("A"), variant.to::().as_deref()); + + let variant = ToVarStrOwned::B.owned_to_variant(); + assert_eq!(Some("B"), variant.to::().as_deref()); + + let variant = ToVarStrOwned::C.owned_to_variant(); + assert_eq!(Some("C"), variant.to::().as_deref()); + + assert_eq!(Some(ToVarStr::A), Variant::new("A").to::()); + assert_eq!(Some(ToVarStr::B), Variant::new("B").to::()); + assert_eq!(Some(ToVarStr::C), Variant::new("C").to::()); + assert_eq!(None, Variant::new("").to::()); + assert_eq!(None, Variant::new("D").to::()); +}} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- + crate::godot_itest! { test_derive_owned_to_variant { #[derive(OwnedToVariant)] struct ToVar {