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 all 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
61 changes: 56 additions & 5 deletions gdnative-core/src/export/property/hint.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Strongly typed property hints.

use std::fmt::{self, Write};
use std::fmt::{self, Display, Write};
use std::ops::RangeInclusive;

use crate::core_types::GodotString;
Expand Down Expand Up @@ -116,20 +116,26 @@ where
/// ```
#[derive(Clone, Eq, PartialEq, Debug, Default)]
pub struct EnumHint {
values: Vec<String>,
entries: Vec<EnumHintEntry>,
}

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

#[inline]
pub fn with_entries(entries: Vec<EnumHintEntry>) -> Self {
EnumHint { entries }
}

/// Formats the hint as a Godot hint string.
fn to_godot_hint_string(&self) -> GodotString {
let mut s = String::new();

let mut iter = self.values.iter();
let mut iter = self.entries.iter();

if let Some(first) = iter.next() {
write!(s, "{first}").unwrap();
Expand All @@ -143,6 +149,38 @@ impl EnumHint {
}
}

#[derive(Clone, PartialEq, Eq, Debug)]
pub struct EnumHintEntry {
key: String,
value: Option<i64>,
}

impl EnumHintEntry {
#[inline]
pub fn new(key: String) -> Self {
Self { key, value: None }
}

#[inline]
pub fn with_value(key: String, value: i64) -> Self {
Self {
key,
value: Some(value),
}
}
}

impl Display for EnumHintEntry {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.key)?;
if let Some(value) = self.value {
write!(f, ":{}", value)?;
}
Ok(())
}
}

/// Possible hints for integers.
#[derive(Clone, Debug)]
#[non_exhaustive]
Expand Down Expand Up @@ -469,3 +507,16 @@ 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_entries(vec![
EnumHintEntry::with_value("Foo".to_string(), 42),
EnumHintEntry::with_value("Bar".to_string(), 67),
]);
assert_eq!(hint.to_godot_hint_string().to_string(), "Foo:42,Bar:67".to_string(),);
});
177 changes: 177 additions & 0 deletions gdnative-derive/src/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use crate::crate_gdnative_core;
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::ToTokens;
use syn::spanned::Spanned;
use syn::{DeriveInput, Fields, Meta};

#[derive(Copy, Clone, Debug)]
enum Kind {
Enum,
}

#[derive(Debug)]
struct DeriveData {
kind: Kind,
ident: Ident,
data: syn::Data,
}

fn parse_derive_input(input: DeriveInput) -> syn::Result<DeriveData> {
let DeriveInput {
ident, data, attrs, ..
} = input.clone();

let (kind, errors) = attrs
.iter()
.filter(|attr| attr.path.is_ident("export"))
.fold((None, vec![]), |(mut kind, mut errors), attr| {
let list = match attr.parse_meta() {
Ok(meta) => match meta {
Meta::List(list) => list,
Meta::Path(path) => {
errors.push(syn::Error::new(
path.span(),
"missing macro arguments. expected #[export(...)]",
));
return (kind, errors);
}
Meta::NameValue(pair) => {
errors.push(syn::Error::new(
pair.span(),
"missing macro arguments. expected #[export(...)]",
));
return (kind, errors);
}
},
Err(e) => {
errors.push(syn::Error::new(
e.span(),
format!("unknown attribute format. expected #[export(...)]: {e}"),
));
return (kind, errors);
}
};

for meta in list.nested.into_iter() {
let syn::NestedMeta::Meta(Meta::NameValue(pair)) = meta else {
errors.push(syn::Error::new(
meta.span(),
"invalid syntax. expected #[export(key = \"value\")]",
));
continue;
};

if !pair.path.is_ident("kind") {
errors.push(syn::Error::new(
pair.span(),
format!("found {}, expected kind", pair.path.into_token_stream()),
));
continue;
}

let syn::Lit::Str(str) = pair.lit else {
errors.push(syn::Error::new(
pair.lit.span(),
"string literal expected, wrap with double quotes",
));
continue;
};

match str.value().as_str() {
"enum" => {
if kind.is_some() {
errors.push(syn::Error::new(str.span(), "kind already set"));
} else {
kind = Some(Kind::Enum);
}
}
_ => {
errors.push(syn::Error::new(str.span(), "unknown kind, expected enum"));
}
}
}

(kind, errors)
});

if let Some(err) = errors.into_iter().reduce(|mut acc, err| {
acc.combine(err);
acc
}) {
return Err(err);
}

match kind {
Some(kind) => Ok(DeriveData { ident, kind, data }),
None => Err(syn::Error::new(Span::call_site(), "kind not found")),
}
}

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 derive_data = parse_derive_input(input)?;

match derive_data.kind {
Kind::Enum => {
let derived_enum = match derive_data.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(&derive_data.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 gdnative_core = crate_gdnative_core();
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! { #gdnative_core::export::hint::EnumHintEntry::with_value(stringify!(#key).to_string(), #val) }
})
.collect::<Vec<_>>();

let impl_block = quote! {
const _: () = {
pub enum NoHint {}

impl #gdnative_core::export::Export for #enum_ty {
type Hint = NoHint;

#[inline]
fn export_info(_hint: Option<Self::Hint>) -> #gdnative_core::export::ExportInfo {
let mappings = vec![ #(#mappings),* ];
let enum_hint = #gdnative_core::export::hint::EnumHint::with_entries(mappings);
return #gdnative_core::export::hint::IntHint::<i64>::Enum(enum_hint).export_info();
}
}
};
};

Ok(impl_block)
}
59 changes: 59 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,64 @@ 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")]
/// #[export(kind = "enum")]
/// #[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, attributes(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
4 changes: 4 additions & 0 deletions gdnative/tests/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ 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_*.rs");
}

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

#[derive(Export, ToVariant)]
#[export(kind = "enum")]
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:6:5
|
6 | Bar(String),
| ^^^

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

#[derive(Export, ToVariant)]
#[export(kind = "enum")]
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:5:5
|
5 | pub struct Foo {
| ^^^^^^
Loading
Loading