Skip to content

#[derive(Property, Export)] for enums #371

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

Merged
merged 1 commit into from
Aug 6, 2023
Merged
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
77 changes: 77 additions & 0 deletions godot-macros/src/derive_export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use venial::{Declaration, StructFields};

use crate::util::{bail, decl_get_info, DeclInfo};
use crate::ParseResult;

pub fn transform(decl: Declaration) -> ParseResult<TokenStream2> {
let DeclInfo { name, .. } = decl_get_info(&decl);

let enum_ = match decl {
Declaration::Enum(e) => e,
Declaration::Struct(s) => {
return bail!(s.tk_struct, "Export can only be derived on enums for now")
}
Declaration::Union(u) => {
return bail!(u.tk_union, "Export can only be derived on enums for now")
}
_ => unreachable!(),
};

let hint_string = if enum_.variants.is_empty() {
return bail!(
enum_.name,
"In order to derive Export, enums must have at least one variant"
);
} else {
let mut hint_string_segments = Vec::new();
for (enum_v, _) in enum_.variants.inner.iter() {
let v_name = enum_v.name.clone();
let v_disc = if let Some(c) = enum_v.value.clone() {
c.value
} else {
return bail!(
v_name,
"Property can only be derived on enums with explicit discriminants in all their variants"
);
};
let v_disc_trimmed = v_disc
.to_string()
.trim_matches(['(', ')'].as_slice())
.to_string();

hint_string_segments.push(format!("{v_name}:{v_disc_trimmed}"));

match &enum_v.contents {
StructFields::Unit => {}
_ => {
return bail!(
v_name,
"Property can only be derived on enums with only unit variants for now"
)
}
};
}
hint_string_segments.join(",")
};

let out = quote! {
#[allow(unused_parens)]
impl godot::bind::property::Export for #name {
fn default_export_info() -> godot::bind::property::ExportInfo {
godot::bind::property::ExportInfo {
hint: godot::engine::global::PropertyHint::PROPERTY_HINT_ENUM,
hint_string: godot::prelude::GodotString::from(#hint_string),
}
}
}
};
Ok(out)
}
121 changes: 121 additions & 0 deletions godot-macros/src/derive_property.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use venial::{Declaration, StructFields};

use crate::util::{bail, decl_get_info, ident, DeclInfo};
use crate::ParseResult;

pub fn transform(decl: Declaration) -> ParseResult<TokenStream2> {
let DeclInfo {
name, name_string, ..
} = decl_get_info(&decl);

let body_get;
let body_set;
let intermediate;

let enum_ = match decl {
Declaration::Enum(e) => e,
Declaration::Struct(s) => {
return bail!(s.tk_struct, "Property can only be derived on enums for now")
}
Declaration::Union(u) => {
return bail!(u.tk_union, "Property can only be derived on enums for now")
}
_ => unreachable!(),
};

if enum_.variants.is_empty() {
return bail!(
enum_.name,
"In order to derive Property, enums must have at least one variant"
);
} else {
let mut matches_get = quote! {};
let mut matches_set = quote! {};
intermediate = if let Some(attr) = enum_
.attributes
.iter()
.find(|attr| attr.get_single_path_segment() == Some(&ident("repr")))
{
attr.value.to_token_stream()
} else {
return bail!(
name,
"Property can only be derived on enums with an explicit `#[repr(i*/u*)]` type"
);
};

for (enum_v, _) in enum_.variants.inner.iter() {
let v_name = enum_v.name.clone();
let v_disc = if let Some(c) = enum_v.value.clone() {
c.value
} else {
return bail!(
v_name,
"Property can only be derived on enums with explicit discriminants in all their variants"
);
};

let match_content_get;
let match_content_set;
match &enum_v.contents {
StructFields::Unit => {
match_content_get = quote! {
Self::#v_name => #v_disc,
};
match_content_set = quote! {
#v_disc => Self::#v_name,
};
}
_ => {
return bail!(
v_name,
"Property can only be derived on enums with only unit variants for now"
)
}
};
matches_get = quote! {
#matches_get
#match_content_get
};
matches_set = quote! {
#matches_set
#match_content_set
};
}
body_get = quote! {
match &self {
#matches_get
}
};
body_set = quote! {
*self = match value {
#matches_set
_ => panic!("Incorrect conversion from {} to {}", stringify!(#intermediate), #name_string),
}
};
}

let out = quote! {
#[allow(unused_parens)]
impl godot::bind::property::Property for #name {
type Intermediate = #intermediate;

fn get_property(&self) -> #intermediate {
#body_get
}

fn set_property(&mut self, value: #intermediate) {
#body_set
}
}
};
Ok(out)
}
53 changes: 53 additions & 0 deletions godot-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,10 @@ use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use venial::Declaration;

mod derive_export;
mod derive_from_variant;
mod derive_godot_class;
mod derive_property;
mod derive_to_variant;
mod gdextension;
mod godot_api;
Expand Down Expand Up @@ -454,6 +456,57 @@ pub fn derive_from_variant(input: TokenStream) -> TokenStream {
translate(input, derive_from_variant::transform)
}

/// Derive macro for [Property](godot::bind::property::Property) on enums.
///
/// Currently has some tight requirements which are expected to be softened as implementation expands:
/// - Only works for enums, structs aren't supported by this derive macro at the moment.
/// - The enum must have an explicit `#[repr(u*/i*)]` type.
/// - This will likely stay this way, since `isize`, the default repr type, is not a concept in Godot.
/// - The enum variants must not have any fields - currently only unit variants are supported.
/// - The enum variants must have explicit discriminants, that is, e.g. `A = 2`, not just `A`
///
/// # Example
///
/// ```no_run
/// # use godot::prelude::*;
/// #[repr(i32)]
/// #[derive(Property)]
/// # #[derive(PartialEq, Eq, Debug)]
/// enum TestEnum {
/// A = 0,
/// B = 1,
/// }
///
/// #[derive(GodotClass)]
/// struct TestClass {
/// #[var]
/// foo: TestEnum
/// }
///
/// # //TODO: remove this when https://github.com/godot-rust/gdext/issues/187 is truly addressed
/// # #[godot_api]
/// # impl TestClass {}
///
/// # fn main() {
/// let mut class = TestClass {foo: TestEnum::B};
/// assert_eq!(class.get_foo(), TestEnum::B as i32);
/// class.set_foo(TestEnum::A as i32);
/// assert_eq!(class.foo, TestEnum::A);
/// # }
/// ```
#[proc_macro_derive(Property)]
pub fn derive_property(input: TokenStream) -> TokenStream {
translate(input, derive_property::transform)
}

/// Derive macro for [Export](godot::bind::property::Property) on enums.
///
/// Currently has some tight requirements which are expected to be softened as implementation expands, see requirements for [Property]
#[proc_macro_derive(Export)]
pub fn derive_export(input: TokenStream) -> TokenStream {
translate(input, derive_export::transform)
}

#[proc_macro_attribute]
pub fn godot_api(_meta: TokenStream, input: TokenStream) -> TokenStream {
translate(input, godot_api::transform)
Expand Down
4 changes: 2 additions & 2 deletions godot/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ pub mod init {
/// Export user-defined classes and methods to be called by the engine.
pub mod bind {
pub use godot_core::property;
pub use godot_macros::{godot_api, FromVariant, GodotClass, ToVariant};
pub use godot_macros::{godot_api, Export, FromVariant, GodotClass, Property, ToVariant};
}

/// Testing facilities (unstable).
Expand All @@ -184,7 +184,7 @@ pub use godot_core::private;
/// Often-imported symbols.
pub mod prelude {
pub use super::bind::property::{Export, Property, TypeStringHint};
pub use super::bind::{godot_api, FromVariant, GodotClass, ToVariant};
pub use super::bind::{godot_api, Export, FromVariant, GodotClass, Property, ToVariant};

pub use super::builtin::math::FloatExt as _;
pub use super::builtin::*;
Expand Down
75 changes: 75 additions & 0 deletions itest/rust/src/property_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use godot::{
bind::property::ExportInfo,
engine::{global::PropertyHint, Texture},
prelude::*,
test::itest,
};

// No tests currently, tests using these classes are in Godot scripts.
Expand Down Expand Up @@ -293,3 +294,77 @@ struct CheckAllExports {

#[godot_api]
impl CheckAllExports {}

#[repr(i64)]
#[derive(Property, Debug, PartialEq, Eq, Export)]
pub enum TestEnum {
A = 0,
B = 1,
C = 2,
}

#[derive(GodotClass)]
pub struct DeriveProperty {
#[var]
pub foo: TestEnum,
}

#[godot_api]
impl DeriveProperty {}

#[itest]
fn derive_property() {
let mut class = DeriveProperty { foo: TestEnum::B };
assert_eq!(class.get_foo(), TestEnum::B as i64);
class.set_foo(TestEnum::C as i64);
assert_eq!(class.foo, TestEnum::C);
}

#[derive(GodotClass)]
pub struct DeriveExport {
#[export]
pub foo: TestEnum,

#[base]
pub base: Base<RefCounted>,
}

#[godot_api]
impl DeriveExport {}

#[godot_api]
impl RefCountedVirtual for DeriveExport {
fn init(base: godot::obj::Base<Self::Base>) -> Self {
Self {
foo: TestEnum::B,
base,
}
}
}

#[itest]
fn derive_export() {
let class: Gd<DeriveExport> = Gd::new_default();

let property = class
.get_property_list()
.iter_shared()
.find(|c| c.get_or_nil("name") == "foo".to_variant())
.unwrap();
assert_eq!(
property.get_or_nil("class_name"),
"DeriveExport".to_variant()
);
assert_eq!(
property.get_or_nil("type"),
(VariantType::Int as i32).to_variant()
);
assert_eq!(
property.get_or_nil("hint"),
(PropertyHint::PROPERTY_HINT_ENUM.ord()).to_variant()
);
assert_eq!(
property.get_or_nil("hint_string"),
"A:0,B:1,C:2".to_variant()
);
}