Skip to content

Commit

Permalink
Try #775:
Browse files Browse the repository at this point in the history
  • Loading branch information
bors[bot] authored Oct 17, 2023
2 parents 6dc9721 + 348bac2 commit 83eccfd
Show file tree
Hide file tree
Showing 24 changed files with 475 additions and 5 deletions.
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 };
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

0 comments on commit 83eccfd

Please sign in to comment.