Skip to content

Commit de96485

Browse files
committed
feat(property): enforce it should be used on Property if both get/set are provided
1 parent 5dc6382 commit de96485

File tree

4 files changed

+265
-67
lines changed

4 files changed

+265
-67
lines changed

gdnative-core/src/export/property.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//! Property registration.
2+
use std::marker::PhantomData;
23

34
use accessor::{Getter, RawGetter, RawSetter, Setter};
45
use invalid_accessor::{InvalidGetter, InvalidSetter};
@@ -322,6 +323,12 @@ impl PropertyUsage {
322323
}
323324
}
324325

326+
/// A ZST used to register a property with no backing field for it.
327+
#[derive(Default)]
328+
pub struct Property<T> {
329+
_marker: PhantomData<T>,
330+
}
331+
325332
mod impl_export {
326333
use super::*;
327334

gdnative-derive/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,21 @@ pub fn profiled(meta: TokenStream, input: TokenStream) -> TokenStream {
218218
/// Call hook methods with `self` and `owner` before and/or after the generated property
219219
/// accessors.
220220
///
221+
/// - `get` / `get_ref` / `set`
222+
///
223+
/// Configure getter/setter for property. All of them can accept a path to specify a custom
224+
/// property accessor. For example, `#[property(get = "Self::my_getter")]` will use
225+
/// `Self::my_getter` as the getter.
226+
///
227+
/// The difference of `get` and `get_ref` is that `get` will register the getter with
228+
/// `with_getter` function, which means your getter should return an owned value `T`, but
229+
/// `get_ref` use `with_ref_getter` to register getter. In this case, your custom getter
230+
/// should return a shared reference `&T`.
231+
///
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.
235+
///
221236
/// - `no_editor`
222237
///
223238
/// Hides the property from the editor. Does not prevent it from being sent over network or saved in storage.

gdnative-derive/src/native_script.rs

Lines changed: 120 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -61,78 +61,112 @@ pub(crate) fn derive_native_class(derive_input: &DeriveInput) -> Result<TokenStr
6161
.register_callback
6262
.map(|function_path| quote!(#function_path(builder);))
6363
.unwrap_or(quote!({}));
64-
let properties = data.properties.into_iter().map(|(ident, config)| {
65-
let with_default = config
66-
.default
67-
.map(|default_value| quote!(.with_default(#default_value)));
68-
let with_hint = config.hint.map(|hint_fn| quote!(.with_hint(#hint_fn())));
69-
let with_usage = if config.no_editor {
70-
Some(quote!(.with_usage(::gdnative::export::PropertyUsage::NOEDITOR)))
71-
} else {
72-
None
73-
};
74-
// if both of them are not set, i.e. `#[property]`. implicitly use both getter/setter
75-
let (get, set) = if config.get.is_none() && config.set.is_none() {
76-
(Some(PropertyGet::Default), Some(PropertySet::Default))
77-
} else {
78-
(config.get, config.set)
79-
};
80-
let before_get: Option<Stmt> = config
81-
.before_get
82-
.map(|path_expr| parse_quote!(#path_expr(this, _owner);));
83-
let after_get: Option<Stmt> = config
84-
.after_get
85-
.map(|path_expr| parse_quote!(#path_expr(this, _owner);));
86-
let with_getter = get.map(|get| {
87-
let register_fn = match get {
88-
PropertyGet::Owned(_) => quote!(with_getter),
89-
_ => quote!(with_ref_getter),
64+
let properties = data
65+
.properties
66+
.into_iter()
67+
.map(|(ident, config)| {
68+
let with_default = config
69+
.default
70+
.map(|default_value| quote!(.with_default(#default_value)));
71+
let with_hint = config.hint.map(|hint_fn| quote!(.with_hint(#hint_fn())));
72+
let with_usage = if config.no_editor {
73+
Some(quote!(.with_usage(::gdnative::export::PropertyUsage::NOEDITOR)))
74+
} else {
75+
None
9076
};
91-
let get: Expr = match get {
92-
PropertyGet::Default => parse_quote!(&this.#ident),
93-
PropertyGet::Owned(path_expr) | PropertyGet::Ref(path_expr) => {
94-
parse_quote!(#path_expr(this, _owner))
95-
}
77+
// check whether this property type is `Property<T>`. if so, extract T from it.
78+
let property_ty = match config.ty {
79+
Type::Path(ref path) => path
80+
.path
81+
.segments
82+
.iter()
83+
.last()
84+
.filter(|seg| seg.ident == "Property")
85+
.and_then(|seg| match seg.arguments {
86+
syn::PathArguments::AngleBracketed(ref params) => params.args.first(),
87+
_ => None,
88+
})
89+
.and_then(|arg| match arg {
90+
syn::GenericArgument::Type(ref ty) => Some(ty),
91+
_ => None,
92+
})
93+
.map(|ty| quote!(::<#ty>)),
94+
_ => None,
9695
};
97-
quote!(
98-
.#register_fn(|this: &#name, _owner: ::gdnative::object::TRef<Self::Base>| {
99-
#before_get
100-
let res = #get;
101-
#after_get
102-
res
103-
})
104-
)
105-
});
106-
let before_set: Option<Stmt> = config
107-
.before_set
108-
.map(|path_expr| parse_quote!(#path_expr(this, _owner);));
109-
let after_set: Option<Stmt> = config
110-
.after_set
111-
.map(|path_expr| parse_quote!(#path_expr(this, _owner);));
112-
let with_setter = set.map(|set| {
113-
let set: Stmt = match set {
114-
PropertySet::Default => parse_quote!(this.#ident = v;),
115-
PropertySet::WithPath(path_expr) => parse_quote!(#path_expr(this, _owner, v);),
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)
102+
{
103+
return Err(syn::Error::new(
104+
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)."
107+
));
108+
}
109+
// 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() {
111+
(Some(PropertyGet::Default), Some(PropertySet::Default))
112+
} else {
113+
(config.get, config.set)
116114
};
117-
quote!(
118-
.with_setter(|this: &mut #name, _owner: ::gdnative::object::TRef<Self::Base>, v| {
119-
#before_set
120-
#set
121-
#after_set
115+
let before_get: Option<Stmt> = config
116+
.before_get
117+
.map(|path_expr| parse_quote!(#path_expr(this, _owner);));
118+
let after_get: Option<Stmt> = config
119+
.after_get
120+
.map(|path_expr| parse_quote!(#path_expr(this, _owner);));
121+
let with_getter = get.map(|get| {
122+
let register_fn = match get {
123+
PropertyGet::Owned(_) => quote!(with_getter),
124+
_ => quote!(with_ref_getter),
125+
};
126+
let get: Expr = match get {
127+
PropertyGet::Default => parse_quote!(&this.#ident),
128+
PropertyGet::Owned(path_expr) | PropertyGet::Ref(path_expr) => parse_quote!(#path_expr(this, _owner))
129+
};
130+
quote!(
131+
.#register_fn(|this: &#name, _owner: ::gdnative::object::TRef<Self::Base>| {
132+
#before_get
133+
let res = #get;
134+
#after_get
135+
res
136+
})
137+
)
138+
});
139+
let before_set: Option<Stmt> = config
140+
.before_set
141+
.map(|path_expr| parse_quote!(#path_expr(this, _owner);));
142+
let after_set: Option<Stmt> = config
143+
.after_set
144+
.map(|path_expr| parse_quote!(#path_expr(this, _owner);));
145+
let with_setter = set.map(|set| {
146+
let set: Stmt = match set {
147+
PropertySet::Default => parse_quote!(this.#ident = v;),
148+
PropertySet::WithPath(path_expr) => parse_quote!(#path_expr(this, _owner, v);),
149+
};
150+
quote!(
151+
.with_setter(|this: &mut #name, _owner: ::gdnative::object::TRef<Self::Base>, v| {
152+
#before_set
153+
#set
154+
#after_set
155+
}))
156+
});
157+
158+
let label = config.path.unwrap_or_else(|| format!("{}", ident));
159+
Ok(quote!({
160+
builder.property#property_ty(#label)
161+
#with_default
162+
#with_hint
163+
#with_usage
164+
#with_getter
165+
#with_setter
166+
.done();
122167
}))
123-
});
124-
125-
let label = config.path.unwrap_or_else(|| format!("{}", ident));
126-
quote!({
127-
builder.property(#label)
128-
#with_default
129-
#with_hint
130-
#with_usage
131-
#with_getter
132-
#with_setter
133-
.done();
134168
})
135-
});
169+
.collect::<Result<Vec<_>, _>>()?;
136170

137171
let maybe_statically_named = data.godot_name.map(|name_str| {
138172
quote! {
@@ -407,4 +441,23 @@ mod tests {
407441
let input: DeriveInput = syn::parse2(input).unwrap();
408442
parse_derive_input(&input).unwrap();
409443
}
444+
445+
#[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+
);
462+
}
410463
}

test/src/test_derive.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::cell::Cell;
22

3+
use gdnative::export::Property;
34
use gdnative::prelude::*;
45

56
pub(crate) fn run_tests() -> bool {
@@ -9,13 +10,17 @@ pub(crate) fn run_tests() -> bool {
910
status &= test_derive_owned_to_variant();
1011
status &= test_derive_nativeclass_with_property_hooks();
1112
status &= test_derive_nativeclass_without_constructor();
13+
status &= test_derive_nativeclass_with_property_get_set();
14+
status &= test_derive_nativeclass_property_with_only_getter();
1215

1316
status
1417
}
1518

1619
pub(crate) fn register(handle: InitHandle) {
1720
handle.add_class::<PropertyHooks>();
1821
handle.add_class::<EmplacementOnly>();
22+
handle.add_class::<CustomGetSet>();
23+
handle.add_class::<MyVec>();
1924
}
2025

2126
fn test_derive_to_variant() -> bool {
@@ -322,3 +327,121 @@ fn test_derive_nativeclass_without_constructor() -> bool {
322327

323328
ok
324329
}
330+
331+
#[derive(NativeClass)]
332+
#[inherit(Node)]
333+
struct CustomGetSet {
334+
pub get_called: Cell<i32>,
335+
pub set_called: Cell<i32>,
336+
#[allow(dead_code)]
337+
#[property(get_ref = "Self::get_foo", set = "Self::set_foo")]
338+
pub foo: Property<i32>,
339+
pub _foo: i32,
340+
}
341+
342+
#[methods]
343+
impl CustomGetSet {
344+
fn new(_onwer: &Node) -> Self {
345+
Self {
346+
get_called: Cell::new(0),
347+
set_called: Cell::new(0),
348+
foo: Property::default(),
349+
_foo: 0,
350+
}
351+
}
352+
353+
fn get_foo(&self, _owner: TRef<Node>) -> &i32 {
354+
self.get_called.set(self.get_called.get() + 1);
355+
&self._foo
356+
}
357+
358+
fn set_foo(&mut self, _owner: TRef<Node>, value: i32) {
359+
self.set_called.set(self.set_called.get() + 1);
360+
self._foo = value;
361+
}
362+
}
363+
364+
fn test_derive_nativeclass_with_property_get_set() -> bool {
365+
println!(" -- test_derive_nativeclass_with_property_get_set");
366+
let ok = std::panic::catch_unwind(|| {
367+
use gdnative::export::user_data::Map;
368+
let (owner, script) = CustomGetSet::new_instance().decouple();
369+
script
370+
.map(|script| {
371+
assert_eq!(0, script.get_called.get());
372+
assert_eq!(0, script.set_called.get());
373+
})
374+
.unwrap();
375+
owner.set("foo", 1);
376+
script
377+
.map(|script| {
378+
assert_eq!(0, script.get_called.get());
379+
assert_eq!(1, script.set_called.get());
380+
assert_eq!(1, script._foo);
381+
})
382+
.unwrap();
383+
assert_eq!(1, i32::from_variant(&owner.get("foo")).unwrap());
384+
script
385+
.map(|script| {
386+
assert_eq!(1, script.get_called.get());
387+
assert_eq!(1, script.set_called.get());
388+
})
389+
.unwrap();
390+
owner.free();
391+
})
392+
.is_ok();
393+
394+
if !ok {
395+
godot_error!(" !! Test test_derive_nativeclass_with_property_get_set failed");
396+
}
397+
398+
ok
399+
}
400+
401+
#[derive(NativeClass)]
402+
struct MyVec {
403+
vec: Vec<i32>,
404+
#[allow(dead_code)]
405+
#[property(get = "Self::get_size")]
406+
size: Property<u32>,
407+
}
408+
409+
#[methods]
410+
impl MyVec {
411+
fn new(_owner: TRef<Reference>) -> Self {
412+
Self {
413+
vec: Vec::new(),
414+
size: Property::default(),
415+
}
416+
}
417+
418+
fn add(&mut self, val: i32) {
419+
self.vec.push(val);
420+
}
421+
422+
fn get_size(&self, _owner: TRef<Reference>) -> u32 {
423+
self.vec.len() as u32
424+
}
425+
}
426+
427+
fn test_derive_nativeclass_property_with_only_getter() -> bool {
428+
println!(" -- test_derive_nativeclass_property_with_only_getter");
429+
430+
let ok = std::panic::catch_unwind(|| {
431+
use gdnative::export::user_data::MapMut;
432+
let (owner, script) = MyVec::new_instance().decouple();
433+
assert_eq!(u32::from_variant(&owner.get("size")).unwrap(), 0);
434+
script.map_mut(|script| script.add(42)).unwrap();
435+
assert_eq!(u32::from_variant(&owner.get("size")).unwrap(), 1);
436+
// check the setter doesn't work for `size`
437+
let _ = std::panic::catch_unwind(|| owner.set("size", 3));
438+
assert_eq!(u32::from_variant(&owner.get("size")).unwrap(), 1);
439+
})
440+
.is_ok();
441+
442+
if !ok {
443+
godot_error!(" !! Test test_derive_nativeclass_property_with_only_getter failed");
444+
}
445+
446+
ok
447+
}

0 commit comments

Comments
 (0)