Skip to content

Commit

Permalink
Merge pull request #371 from patataofcourse/derive_property_export
Browse files Browse the repository at this point in the history
#[derive(Property, Export)] for enums
  • Loading branch information
Bromeon authored Aug 6, 2023
2 parents 38f7a57 + e44194a commit 7a44ea8
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 2 deletions.
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()
);
}

0 comments on commit 7a44ea8

Please sign in to comment.