Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enum property with custom int discriminants #775

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions gdnative-core/src/export/property/hint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,22 @@ where
/// ```
#[derive(Clone, Eq, PartialEq, Debug, Default)]
pub struct EnumHint {
values: Vec<String>,
values: Vec<(String, Option<i64>)>,
}

impl EnumHint {
#[inline]
pub fn new(values: Vec<String>) -> Self {
let values = values.into_iter().map(|v| (v, None)).collect();
EnumHint { values }
}

#[inline]
pub fn with_numbers(values: Vec<(String, i64)>) -> Self {
let values = values
.into_iter()
.map(|(key, val)| (key, Some(val)))
.collect();
EnumHint { values }
}
chitoyuu marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -130,13 +140,22 @@ impl EnumHint {
let mut s = String::new();

let mut iter = self.values.iter();
let write_item = |s: &mut String, item: &(String, Option<i64>)| match item {
(key, Some(val)) => {
write!(s, "{key}:{val}")
}
(key, None) => {
write!(s, "{key}")
}
};
chitoyuu marked this conversation as resolved.
Show resolved Hide resolved

if let Some(first) = iter.next() {
write!(s, "{first}").unwrap();
write_item(&mut s, first).unwrap();
}

for rest in iter {
write!(s, ",{rest}").unwrap();
write!(s, ",").unwrap();
write_item(&mut s, rest).unwrap();
}

s.into()
Expand Down Expand Up @@ -469,3 +488,13 @@ impl ArrayHint {
}
}
}

godot_test!(test_enum_hint_without_mapping {
let hint = EnumHint::new(vec!["Foo".into(), "Bar".into()]);
assert_eq!(hint.to_godot_hint_string().to_string(), "Foo,Bar".to_string(),);
});

godot_test!(test_enum_hint_with_mapping {
let hint = EnumHint::with_numbers(vec![("Foo".into(), 42), ("Bar".into(), 67)]);
assert_eq!(hint.to_godot_hint_string().to_string(), "Foo:42,Bar:67".to_string(),);
});
65 changes: 65 additions & 0 deletions gdnative-derive/src/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use proc_macro2::{Span, TokenStream as TokenStream2};
use syn::spanned::Spanned;
use syn::{DeriveInput, Fields};

fn err_only_supports_fieldless_enums(span: Span) -> syn::Error {
syn::Error::new(span, "#[derive(Export)] only supports fieldless enums")
}

pub(crate) fn derive_export(input: &DeriveInput) -> syn::Result<TokenStream2> {
let derived_enum = match &input.data {
syn::Data::Enum(data) => data,
syn::Data::Struct(data) => {
return Err(err_only_supports_fieldless_enums(data.struct_token.span()));
}
syn::Data::Union(data) => {
return Err(err_only_supports_fieldless_enums(data.union_token.span()));
}
};

let export_impl = impl_export(&input.ident, derived_enum)?;
Ok(export_impl)
}

fn impl_export(enum_ty: &syn::Ident, data: &syn::DataEnum) -> syn::Result<TokenStream2> {
let err = data
.variants
.iter()
.filter(|variant| !matches!(variant.fields, Fields::Unit))
.map(|variant| err_only_supports_fieldless_enums(variant.ident.span()))
.reduce(|mut acc, err| {
acc.combine(err);
acc
});
if let Some(err) = err {
return Err(err);
}

let mappings = data
.variants
.iter()
.map(|variant| {
let key = &variant.ident;
let val = quote! { #enum_ty::#key as i64 };
chitoyuu marked this conversation as resolved.
Show resolved Hide resolved
quote! { (stringify!(#key).to_string(), #val) }
})
.collect::<Vec<_>>();

let impl_block = quote! {
impl ::gdnative::export::Export for #enum_ty {
type Hint = ::gdnative::export::hint::IntHint<i64>;
#[inline]
fn export_info(hint: Option<Self::Hint>) -> ::gdnative::export::ExportInfo {
if let Some(hint) = hint {
return hint.export_info();
chitoyuu marked this conversation as resolved.
Show resolved Hide resolved
} else {
let mappings = vec![ #(#mappings),* ];
let enum_hint = ::gdnative::export::hint::EnumHint::with_numbers(mappings);
return ::gdnative::export::hint::IntHint::<i64>::Enum(enum_hint).export_info();
}
}
}
};

Ok(impl_block)
}
58 changes: 58 additions & 0 deletions gdnative-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use proc_macro2::TokenStream as TokenStream2;
use quote::ToTokens;
use syn::{parse::Parser, AttributeArgs, DeriveInput, ItemFn, ItemImpl, ItemType};

mod export;
mod init;
mod methods;
mod native_script;
Expand Down Expand Up @@ -663,6 +664,63 @@ pub fn godot_wrap_method(input: TokenStream) -> TokenStream {
}
}

/// Make a rust `enum` has drop-down list in Godot editor.
/// Note that the derived `enum` should also implements `Copy` trait.
///
/// Take the following example, you will see a drop-down list for the `dir`
/// property, and `Up` and `Down` converts to `1` and `-1` in the GDScript
/// side.
///
/// ```
/// use gdnative::prelude::*;
///
/// #[derive(Debug, PartialEq, Clone, Copy, Export, ToVariant, FromVariant)]
/// #[variant(enum = "repr")]
/// #[repr(i32)]
/// enum Dir {
/// Up = 1,
/// Down = -1,
/// }
///
/// #[derive(NativeClass)]
/// #[no_constructor]
/// struct Move {
/// #[property]
/// pub dir: Dir,
/// }
/// ```
///
/// You can't derive `Export` on `enum` that has non-unit variant.
///
/// ```compile_fail
/// use gdnative::prelude::*;
///
/// #[derive(Debug, PartialEq, Clone, Copy, Export)]
/// enum Action {
/// Move((f32, f32, f32)),
/// Attack(u64),
/// }
/// ```
///
/// You can't derive `Export` on `struct` or `union`.
///
/// ```compile_fail
/// use gdnative::prelude::*;
///
/// #[derive(Export)]
/// struct Foo {
/// f1: i32
/// }
/// ```
#[proc_macro_derive(Export)]
pub fn derive_export(input: TokenStream) -> TokenStream {
let derive_input = syn::parse_macro_input!(input as syn::DeriveInput);
match export::derive_export(&derive_input) {
Ok(stream) => stream.into(),
Err(err) => err.to_compile_error().into(),
}
}

/// Returns a standard header for derived implementations.
///
/// Adds the `automatically_derived` attribute and prevents common lints from triggering
Expand Down
6 changes: 6 additions & 0 deletions gdnative/tests/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ fn ui_tests() {
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");

// Export
t.pass("tests/ui/export_pass.rs");
t.compile_fail("tests/ui/export_fail_01.rs");
t.compile_fail("tests/ui/export_fail_02.rs");
t.compile_fail("tests/ui/export_fail_03.rs");
}

// FIXME(rust/issues/54725): Full path spans are only available on nightly as of now
Expand Down
9 changes: 9 additions & 0 deletions gdnative/tests/ui/export_fail_01.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use gdnative::prelude::*;

#[derive(Export, ToVariant)]
pub enum Foo {
Bar(String),
Baz { a: i32, b: u32 },
}

fn main() {}
11 changes: 11 additions & 0 deletions gdnative/tests/ui/export_fail_01.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
error: #[derive(Export)] only supports fieldless enums
--> tests/ui/export_fail_01.rs:5:5
|
5 | Bar(String),
| ^^^

error: #[derive(Export)] only supports fieldless enums
--> tests/ui/export_fail_01.rs:6:5
|
6 | Baz { a: i32, b: u32 },
| ^^^
8 changes: 8 additions & 0 deletions gdnative/tests/ui/export_fail_02.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use gdnative::prelude::*;

#[derive(Export, ToVariant)]
pub struct Foo {
bar: i32,
}

fn main() {}
5 changes: 5 additions & 0 deletions gdnative/tests/ui/export_fail_02.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
error: #[derive(Export)] only supports fieldless enums
--> tests/ui/export_fail_02.rs:4:5
|
4 | pub struct Foo {
| ^^^^^^
8 changes: 8 additions & 0 deletions gdnative/tests/ui/export_fail_03.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use gdnative::prelude::*;

#[derive(Export, ToVariant)]
pub union Foo {
bar: i32,
}

fn main() {}
11 changes: 11 additions & 0 deletions gdnative/tests/ui/export_fail_03.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
error: #[derive(Export)] only supports fieldless enums
--> tests/ui/export_fail_03.rs:4:5
|
4 | pub union Foo {
| ^^^^^

error: Variant conversion derive macro does not work on unions.
--> tests/ui/export_fail_03.rs:4:1
|
4 | pub union Foo {
| ^^^
11 changes: 11 additions & 0 deletions gdnative/tests/ui/export_pass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use gdnative::prelude::*;

#[derive(Export, ToVariant, Clone, Copy)]
#[variant(enum = "repr")]
#[repr(i32)]
pub enum Foo {
Bar,
Baz,
}

fn main() {}
7 changes: 7 additions & 0 deletions test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod test_as_arg;
mod test_async;
mod test_constructor;
mod test_derive;
mod test_export_enum;
mod test_free_ub;
mod test_generic_class;
mod test_indexed_props;
Expand All @@ -28,6 +29,11 @@ pub extern "C" fn run_tests(

status &= gdnative::core_types::test_core_types();

status &= gdnative::export::hint::test_enum_hint_without_mapping();
status &= gdnative::export::hint::test_enum_hint_with_mapping();

status &= test_underscore_method_binding();
status &= test_rust_class_construction();
status &= test_from_instance_id();
status &= test_nil_object_return_value();
status &= test_rust_class_construction();
Expand All @@ -47,6 +53,7 @@ pub extern "C" fn run_tests(
status &= test_vararray_return::run_tests();
status &= test_variant_call_args::run_tests();
status &= test_variant_ops::run_tests();
status &= test_export_enum::run_tests();

Variant::new(status).leak()
}
Expand Down
30 changes: 30 additions & 0 deletions test/src/test_export_enum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use gdnative::prelude::*;

#[derive(Debug, PartialEq, Clone, Copy, Export, ToVariant, FromVariant)]
#[variant(enum = "repr")]
#[repr(i32)]
enum Dir {
Up = 1,
Down = -1,
}

pub(crate) fn run_tests() -> bool {
let mut ok = true;

ok &= test_from_variant();
ok &= test_to_variant();

ok
}

crate::godot_itest!(test_from_variant {
assert_eq!(Dir::from_variant(&1_i32.to_variant()), Ok(Dir::Up));
assert_eq!(Dir::from_variant(&(-1_i32).to_variant()), Ok(Dir::Down));
// 42 isn't mapped to any variant of `Dir`
assert!(Dir::from_variant(&42_i32.to_variant()).is_err());
});

crate::godot_itest!(test_to_variant {
assert_eq!(Dir::Up.to_variant(), 1_i32.to_variant());
assert_eq!(Dir::Down.to_variant(), (-1_i32).to_variant());
});
chitoyuu marked this conversation as resolved.
Show resolved Hide resolved
Loading