From ff000319ee37dae002c7e87b03453517b9263155 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 25 Jan 2024 16:55:49 +0800 Subject: [PATCH] Refine derive macro. --- codegen/src/custom_type.rs | 137 +++++++++++++++++++++++++---------- codegen/src/lib.rs | 11 ++- codegen/tests/test_derive.rs | 22 ++++-- tests/build_type.rs | 70 +++++++++++++++++- 4 files changed, 194 insertions(+), 46 deletions(-) diff --git a/codegen/src/custom_type.rs b/codegen/src/custom_type.rs index 7982e5056..a4fb5d433 100644 --- a/codegen/src/custom_type.rs +++ b/codegen/src/custom_type.rs @@ -6,38 +6,100 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream { let name = input.ident; let accessors = match input.data { - syn::Data::Struct(ref data) => match data.fields { - syn::Fields::Named(ref fields) => { - let iter = fields.named.iter().map(|field| { - let mut get_fn = None; - let mut set_fn = None; - let mut readonly = false; - for attr in field.attrs.iter() { - if attr.path().is_ident("get") { - get_fn = Some( - attr.parse_args() - .unwrap_or_else(syn::Error::into_compile_error), - ); - } else if attr.path().is_ident("set") { - set_fn = Some( - attr.parse_args() - .unwrap_or_else(syn::Error::into_compile_error), - ); - } else if attr.path().is_ident("readonly") { - readonly = true; + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Named(ref fields), + .. + }) => { + let iter = fields.named.iter().map(|field| { + let mut name = None; + let mut get_fn = None; + let mut set_fn = None; + let mut readonly = false; + let mut skip = false; + + for attr in field.attrs.iter() { + if attr.path().is_ident("rhai_custom_type_skip") { + if get_fn.is_some() || set_fn.is_some() || name.is_some() { + return syn::Error::new( + Span::call_site(), + "cannot use 'rhai_custom_type_skip' with other attributes", + ) + .into_compile_error(); } + + skip = true; + continue; } - generate_accessor_fns(&field.ident.as_ref().unwrap(), get_fn, set_fn, readonly) - }); - quote! {#(#iter)*} - } - syn::Fields::Unnamed(_) => { - syn::Error::new(Span::call_site(), "tuple structs are not yet implemented") - .into_compile_error() - } - syn::Fields::Unit => quote! {}, - }, + if skip { + return syn::Error::new( + Span::call_site(), + "cannot use 'rhai_custom_type_skip' with other attributes", + ) + .into_compile_error(); + } + + if attr.path().is_ident("rhai_custom_type_name") { + name = Some( + attr.parse_args() + .unwrap_or_else(syn::Error::into_compile_error), + ); + } else if attr.path().is_ident("rhai_custom_type_get") { + get_fn = Some( + attr.parse_args() + .unwrap_or_else(syn::Error::into_compile_error), + ); + } else if attr.path().is_ident("rhai_custom_type_set") { + if readonly { + return syn::Error::new( + Span::call_site(), + "cannot use 'rhai_custom_type_set' with 'rhai_custom_type_readonly'", + ) + .into_compile_error(); + } + set_fn = Some( + attr.parse_args() + .unwrap_or_else(syn::Error::into_compile_error), + ); + } else if attr.path().is_ident("rhai_custom_type_readonly") { + if set_fn.is_some() { + return syn::Error::new( + Span::call_site(), + "cannot use 'rhai_custom_type_readonly' with 'rhai_custom_type_set'", + ) + .into_compile_error(); + } + readonly = true; + } + } + + if !skip { + generate_accessor_fns( + &field.ident.as_ref().unwrap(), + name, + get_fn, + set_fn, + readonly, + ) + } else { + quote! {} + } + }); + + quote! {#(#iter)*} + } + + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Unnamed(_), + .. + }) => syn::Error::new(Span::call_site(), "tuple structs are not yet implemented") + .into_compile_error(), + + syn::Data::Struct(syn::DataStruct { + fields: syn::Fields::Unit, + .. + }) => quote! {}, + syn::Data::Enum(_) => { syn::Error::new(Span::call_site(), "enums are not yet implemented").into_compile_error() } @@ -47,8 +109,8 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream { }; quote! { - impl ::rhai::CustomType for #name { - fn build(mut builder: ::rhai::TypeBuilder) { + impl CustomType for #name { + fn build(mut builder: TypeBuilder) { #accessors; } } @@ -57,6 +119,7 @@ pub fn derive_custom_type_impl(input: DeriveInput) -> TokenStream { fn generate_accessor_fns( field: &Ident, + name: Option, get: Option, set: Option, readonly: bool, @@ -69,17 +132,17 @@ fn generate_accessor_fns( .map(|func| quote! {#func}) .unwrap_or_else(|| quote! {|obj: &mut Self, val| obj.#field = val}); + let name = name + .map(|field| quote! { #field }) + .unwrap_or_else(|| quote! { stringify!(#field) }); + if readonly { quote! { - builder.with_get("#field", #get); + builder.with_get(#name, #get); } } else { quote! { - builder.with_get_set( - "#field", - #get, - #set, - ); + builder.with_get_set(#name, #get, #set); } } } diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs index 29088aaa1..c72c05bff 100644 --- a/codegen/src/lib.rs +++ b/codegen/src/lib.rs @@ -410,7 +410,16 @@ pub fn set_exported_global_fn(args: proc_macro::TokenStream) -> proc_macro::Toke } } -#[proc_macro_derive(CustomType, attributes(get, set, readonly))] +#[proc_macro_derive( + CustomType, + attributes( + rhai_custom_type_name, + rhai_custom_type_get, + rhai_custom_type_set, + rhai_custom_type_readonly, + rhai_custom_type_skip + ) +)] pub fn derive_custom_type(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); let expanded = custom_type::derive_custom_type_impl(input); diff --git a/codegen/tests/test_derive.rs b/codegen/tests/test_derive.rs index c24938284..262e3f8a6 100644 --- a/codegen/tests/test_derive.rs +++ b/codegen/tests/test_derive.rs @@ -1,15 +1,25 @@ -use rhai_codegen::CustomType; +use rhai::{CustomType, TypeBuilder, INT}; // Sanity check to make sure everything compiles #[derive(Clone, CustomType)] pub struct Foo { - #[get(get_bar)] - bar: i32, - #[readonly] + #[rhai_custom_type_skip] + _dummy: INT, + #[rhai_custom_type_get(get_bar)] + bar: INT, + #[rhai_custom_type_name("boo")] + #[rhai_custom_type_readonly] baz: String, - qux: Vec, + #[rhai_custom_type_set(Self::set_qux)] + qux: Vec, } -fn get_bar(_this: &mut Foo) -> i32 { +impl Foo { + pub fn set_qux(&mut self, value: Vec) { + self.qux = value; + } +} + +fn get_bar(_this: &mut Foo) -> INT { 42 } diff --git a/tests/build_type.rs b/tests/build_type.rs index 550943d52..00d6f296a 100644 --- a/tests/build_type.rs +++ b/tests/build_type.rs @@ -1,8 +1,8 @@ #![cfg(not(feature = "no_object"))] -use rhai::{CustomType, Engine, EvalAltResult, Position, TypeBuilder, INT}; +use rhai::{CustomType, Engine, EvalAltResult, Position, Scope, TypeBuilder, INT}; #[test] -fn build_type() { +fn test_build_type() { #[derive(Debug, Clone, PartialEq, Eq)] struct Vec3 { x: INT, @@ -156,3 +156,69 @@ fn build_type() { 6, ); } + +#[test] +fn test_build_type_macro() { + #[derive(Debug, Clone, Eq, PartialEq)] // <- necessary for any custom type + #[derive(CustomType)] // <- auto-implement 'CustomType' + struct Foo { + #[rhai_custom_type_skip] + dummy: i64, // <- skip this field + #[rhai_custom_type_readonly] // <- only auto-implement getters + bar: i64, + #[rhai_custom_type_name("emphasize")] + baz: bool, // <- auto-implement getter/setter for 'baz' + #[rhai_custom_type_set(Self::set_hello)] // <- call custom setter for 'hello' + hello: String, // <- auto-implement getter for 'hello' + } + + impl Foo { + pub fn set_hello(&mut self, value: String) { + self.hello = if self.baz { + let mut s = self.hello.clone(); + s.push_str(&value); + for _ in 0..self.bar { + s.push('!'); + } + s + } else { + value + }; + } + } + + let mut engine = Engine::new(); + engine.build_type::(); + + let mut scope = Scope::new(); + scope.push( + "foo", + Foo { + dummy: 0, + bar: 5, + baz: false, + hello: "hey".to_string(), + }, + ); + + assert_eq!( + engine + .eval_with_scope::( + &mut scope, + r#" + foo.hello = "this should not be seen"; + foo.hello = "world!"; + foo.emphasize = true; + foo.hello = "yo"; + foo + "# + ) + .unwrap(), + Foo { + dummy: 0, + bar: 5, + baz: true, + hello: "world!yo!!!!!".into() + } + ); +}