Skip to content

Commit 768020f

Browse files
committed
Handle all #[property(get, set)] combinations + test
Changes: * Logic that allows/disallows property <-> field type combinations * Unit tests * Create internal proc_macro2 derive that can be used in tests * Add Property to prelude * Extensive documentation for Property
1 parent de96485 commit 768020f

File tree

4 files changed

+198
-38
lines changed

4 files changed

+198
-38
lines changed

gdnative-core/src/export/property.rs

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,8 +323,111 @@ impl PropertyUsage {
323323
}
324324
}
325325

326-
/// A ZST used to register a property with no backing field for it.
327-
#[derive(Default)]
326+
/// Placeholder type for exported properties with no backing field.
327+
///
328+
/// This is the go-to type whenever you want to expose a getter/setter to GDScript, which
329+
/// does not directly map to a field in your struct. Instead of adding a useless field
330+
/// of the corresponding type (which needs initialization, extra space, etc.), you can use
331+
/// an instance of this type as a placeholder.
332+
///
333+
/// `Property` is a zero-sized type (ZST) which has exactly one value: `Property::default()`.
334+
/// It implements most of the basic traits, which allows its enclosing struct to remain
335+
/// composable and derive those traits itself.
336+
///
337+
/// ## When to use `Property<T>` instead of `T`
338+
///
339+
/// The following table shows which combinations of `#[property]` attributes and field types are allowed.
340+
/// In this context, `get` and `set` behave symmetrically, so only one of the combinations is listed.
341+
/// Furthermore, `get_ref` can be used in place of `get`, when it appears with a path.
342+
///
343+
/// Field type ➡ <br> Attributes ⬇ | bare `T` | `Property<T>`
344+
/// ------------------------------------------|-------------------------------|-----------------------------
345+
/// `#[property]` | ✔️ default get + set | ❌️
346+
/// `#[property(get, set)]` _(same as above)_ | ✔️ default get + set | ❌️
347+
/// `#[property(get)]` | ✔️ default get (no set) | ❌️
348+
/// `#[property(get="path")]` | ⚠️ custom get (no set) | ✔️ custom get (no set)
349+
/// `#[property(get="path", set)]` | ✔️ custom get, default set | ❌️
350+
/// `#[property(get="path", set="path")]` | ⚠️ custom get + set | ✔️ custom get + set
351+
///
352+
/// "⚠️" means that this attribute combination is allowed for bare `T`, but you should consider
353+
/// using `Property<T>`.
354+
///
355+
/// Since there is no default `get` or `set` in these cases, godot-rust will never access the field
356+
/// directly. In other words, you are not really exporting _that field_, but linking its name and type
357+
/// (but not its value) to the specified get/set methods.
358+
///
359+
/// To decide when to use which:
360+
/// * If you access your field as-is on the Rust side, use bare `T`.<br>
361+
/// With a `Property<T>` field on the other hand, you would need to _additionally_ add a `T` backing field.
362+
/// * If you don't need a backing field, use `Property<T>`.<br>
363+
/// This is the case whenever you compute a result dynamically, or map values between Rust and GDScript
364+
/// representations.
365+
///
366+
/// ## Examples
367+
///
368+
/// Read/write accessible:
369+
/// ```no_run
370+
/// # use gdnative::prelude::*;
371+
/// #[derive(NativeClass)]
372+
/// # #[no_constructor]
373+
/// struct MyObject {
374+
/// #[property]
375+
/// color: Color,
376+
/// }
377+
/// ```
378+
///
379+
/// Read-only:
380+
/// ```no_run
381+
/// # use gdnative::prelude::*;
382+
/// #[derive(NativeClass)]
383+
/// # #[no_constructor]
384+
/// struct MyObject {
385+
/// #[property(get)]
386+
/// hitpoints: f32,
387+
/// }
388+
/// ```
389+
///
390+
/// Read-write, with validating setter:
391+
/// ```no_run
392+
/// # use gdnative::prelude::*;
393+
/// # fn validate(s: &String) -> bool { true }
394+
/// #[derive(NativeClass)]
395+
/// # #[no_constructor]
396+
/// struct MyObject {
397+
/// #[property(get, set = "Self::set_name")]
398+
/// player_name: String,
399+
/// }
400+
///
401+
/// #[methods]
402+
/// impl MyObject {
403+
/// fn set_name(&mut self, _owner: TRef<Reference>, name: String) {
404+
/// if validate(&name) {
405+
/// self.player_name = name;
406+
/// }
407+
/// }
408+
/// }
409+
/// ```
410+
///
411+
/// Write-only, no backing field, custom setter:
412+
/// ```no_run
413+
/// # use gdnative::prelude::*;
414+
/// #[derive(NativeClass)]
415+
/// # #[no_constructor]
416+
/// struct MyObject {
417+
/// #[property(set = "Self::set_password")]
418+
/// password: Property<String>,
419+
/// }
420+
///
421+
/// #[methods]
422+
/// impl MyObject {
423+
/// fn set_password(&mut self, _owner: TRef<Reference>, password: String) {
424+
/// // securely hash and store password
425+
/// }
426+
/// }
427+
/// ```
428+
429+
// Note: traits are mostly implemented to enable deriving the same traits on the enclosing struct.
430+
#[derive(Copy, Clone, Debug, Default, Ord, PartialOrd, Eq, PartialEq, Hash)]
328431
pub struct Property<T> {
329432
_marker: PhantomData<T>,
330433
}

gdnative-derive/src/lib.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,9 @@ pub fn profiled(meta: TokenStream, input: TokenStream) -> TokenStream {
229229
/// `get_ref` use `with_ref_getter` to register getter. In this case, your custom getter
230230
/// should return a shared reference `&T`.
231231
///
232-
/// `get` and `set` can be used without specifying a path, as long as the field type is not
233-
/// `Property<T>`. In this case, godot-rust generates an accessor function for the field.
234-
/// For example, `#[property(get)]` will generate a read-only property.
232+
/// Situations with custom getters/setters and no backing fields require the use of the
233+
/// type [`Property<T>`][gdnative::export::Property]. Consult its documentation for
234+
/// a deeper elaboration of property exporting.
235235
///
236236
/// - `no_editor`
237237
///
@@ -394,19 +394,21 @@ pub fn derive_native_class(input: TokenStream) -> TokenStream {
394394
let derive_input = syn::parse_macro_input!(input as DeriveInput);
395395

396396
// Implement NativeClass for the input
397-
native_script::derive_native_class(&derive_input).map_or_else(
397+
let derived = native_script::derive_native_class(&derive_input).map_or_else(
398398
|err| {
399399
// Silence the other errors that happen because NativeClass is not implemented
400400
let empty_nativeclass = native_script::impl_empty_nativeclass(&derive_input);
401401
let err = err.to_compile_error();
402402

403-
TokenStream::from(quote! {
403+
quote! {
404404
#empty_nativeclass
405405
#err
406-
})
406+
}
407407
},
408408
std::convert::identity,
409-
)
409+
);
410+
411+
TokenStream::from(derived)
410412
}
411413

412414
#[proc_macro_derive(ToVariant, attributes(variant))]

gdnative-derive/src/native_script.rs

Lines changed: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use proc_macro::TokenStream;
21
use proc_macro2::TokenStream as TokenStream2;
32

43
use syn::spanned::Spanned;
@@ -48,7 +47,7 @@ pub(crate) fn impl_empty_nativeclass(derive_input: &DeriveInput) -> TokenStream2
4847
}
4948
}
5049

51-
pub(crate) fn derive_native_class(derive_input: &DeriveInput) -> Result<TokenStream, syn::Error> {
50+
pub(crate) fn derive_native_class(derive_input: &DeriveInput) -> Result<TokenStream2, syn::Error> {
5251
let derived = crate::automatically_derived();
5352
let data = parse_derive_input(derive_input)?;
5453

@@ -93,21 +92,27 @@ pub(crate) fn derive_native_class(derive_input: &DeriveInput) -> Result<TokenStr
9392
.map(|ty| quote!(::<#ty>)),
9493
_ => None,
9594
};
96-
// #[property] is not attached on `Property<T>`
97-
if property_ty.is_none()
98-
// custom getter used
99-
&& config.get.as_ref().map(|get| !matches!(get, PropertyGet::Default)).unwrap_or(false)
100-
// custom setter used
101-
&& config.set.as_ref().map(|set| !matches!(set, PropertySet::Default)).unwrap_or(false)
95+
96+
// Attribute is #[property] (or has other arguments which are not relevant here)
97+
let is_standalone_attribute = config.get.is_none() && config.set.is_none();
98+
// Attribute is #[property(get)] or #[property(get, set="path")]
99+
let has_default_getter = matches!(config.get, Some(PropertyGet::Default));
100+
// Attribute is #[property(set)] or #[property(get="path", set)]
101+
let has_default_setter = matches!(config.set, Some(PropertySet::Default));
102+
103+
// Field type is `Property<T>`
104+
if property_ty.is_some()
105+
&& (is_standalone_attribute || has_default_getter || has_default_setter)
102106
{
103107
return Err(syn::Error::new(
104108
ident.span(),
105-
"The `#[property]` attribute can only be used on a field of type `Property`, \
106-
if a path is provided for both get/set method(s)."
109+
"The `#[property]` attribute requires explicit paths for `get` and `set` argument; \
110+
the defaults #[property], #[property(get)] and #[property(set)] are not allowed."
107111
));
108112
}
113+
109114
// if both of them are not set, i.e. `#[property]`. implicitly use both getter/setter
110-
let (get, set) = if config.get.is_none() && config.set.is_none() {
115+
let (get, set) = if is_standalone_attribute {
111116
(Some(PropertyGet::Default), Some(PropertySet::Default))
112117
} else {
113118
(config.get, config.set)
@@ -206,7 +211,7 @@ pub(crate) fn derive_native_class(derive_input: &DeriveInput) -> Result<TokenStr
206211
};
207212

208213
// create output token stream
209-
Ok(trait_impl.into())
214+
Ok(trait_impl)
210215
}
211216

212217
fn parse_derive_input(input: &DeriveInput) -> Result<DeriveData, syn::Error> {
@@ -443,21 +448,71 @@ mod tests {
443448
}
444449

445450
#[test]
446-
fn derive_property_require_to_be_used_on_property_without_default_accessor() {
447-
let input: TokenStream2 = syn::parse_str(
448-
r#"
449-
#[inherit(Node)]
450-
struct Foo {
451-
#[property(get = "Self::get_bar", set = "Self::set_bar")]
452-
bar: i64,
453-
}"#,
454-
)
455-
.unwrap();
456-
let input: DeriveInput = syn::parse2(input).unwrap();
457-
assert_eq!(
458-
derive_native_class(&input).unwrap_err().to_string(),
459-
"The `#[property]` attribute can only be used on a field of type `Property`, \
460-
if a path is provided for both get/set method(s).",
461-
);
451+
fn derive_property_combinations() {
452+
let attr_none = quote! { #[property] };
453+
let attr_get = quote! { #[property(get )] };
454+
let attr_getp = quote! { #[property(get="path" )] };
455+
let attr_set = quote! { #[property( set )] };
456+
let attr_setp = quote! { #[property( set="path")] };
457+
let attr_get_set = quote! { #[property(get, set )] };
458+
let attr_get_setp = quote! { #[property(get, set="path")] };
459+
let attr_getp_set = quote! { #[property(get="path", set )] };
460+
let attr_getp_setp = quote! { #[property(get="path", set="path")] };
461+
462+
// See documentation of Property<T> for this table
463+
// Columns: #[property] attributes | i32 style fields | Property<i32> style fields
464+
let combinations = [
465+
(attr_none, true, false),
466+
(attr_get, true, false),
467+
(attr_getp, true, true),
468+
(attr_set, true, false),
469+
(attr_setp, true, true),
470+
(attr_get_set, true, false),
471+
(attr_get_setp, true, false),
472+
(attr_getp_set, true, false),
473+
(attr_getp_setp, true, true),
474+
];
475+
476+
for (attr, allowed_bare, allowed_property) in &combinations {
477+
check_property_combination(attr, quote! { i32 }, *allowed_bare);
478+
check_property_combination(attr, quote! { Property<i32> }, *allowed_property);
479+
}
480+
}
481+
482+
/// Tests whether a certain combination of a `#[property]` attribute (attr) and a field type
483+
/// (bare i32 or Property<i32>) should compile successfully
484+
fn check_property_combination(
485+
attr: &TokenStream2,
486+
field_type: TokenStream2,
487+
should_succeed: bool,
488+
) {
489+
// Lazy because of formatting in error message
490+
let input = || {
491+
quote! {
492+
#[inherit(Node)]
493+
struct Foo {
494+
#attr
495+
field: #field_type
496+
}
497+
}
498+
};
499+
500+
let derive_input: DeriveInput = syn::parse2(input()).unwrap();
501+
let derived = derive_native_class(&derive_input);
502+
503+
if should_succeed {
504+
assert!(
505+
derived.is_ok(),
506+
"Valid derive expression fails to compile:\n{}",
507+
input().to_string()
508+
);
509+
} else {
510+
assert_eq!(
511+
derived.unwrap_err().to_string(),
512+
"The `#[property]` attribute requires explicit paths for `get` and `set` argument; \
513+
the defaults #[property], #[property(get)] and #[property(set)] are not allowed.",
514+
"Invalid derive expression compiles by mistake:\n{}", input().to_string()
515+
);
516+
}
462517
}
463518
}

gdnative/src/prelude.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub use gdnative_core::core_types::{
1414
FromVariant, FromVariantError, OwnedToVariant, ToVariant, ToVariantEq,
1515
};
1616
pub use gdnative_core::export::{
17-
ClassBuilder, ExportInfo, Method, MethodBuilder, NativeClass, NativeClassMethods,
17+
ClassBuilder, ExportInfo, Method, MethodBuilder, NativeClass, NativeClassMethods, Property,
1818
PropertyUsage, SignalBuilder, SignalParam,
1919
};
2020
pub use gdnative_core::init::InitHandle;

0 commit comments

Comments
 (0)