Skip to content

Commit 4afc54e

Browse files
committed
match_class! macro to dispatch subclasses
1 parent ea8033b commit 4afc54e

File tree

5 files changed

+207
-2
lines changed

5 files changed

+207
-2
lines changed

godot-core/src/classes/match_class.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
/// Dispatches a class to different subclasses.
9+
///
10+
/// Similar to a `match` statement, but with downcasts. Earlier matches dominate, so keep more-derived classes first.
11+
/// The current implementation checks [`Gd::try_cast()`][crate::obj::Gd::try_cast] linearly with the number of branches.
12+
/// This may change in the future.
13+
///
14+
/// Requires a fallback branch, even if all direct known classes are handled. The reason for this is that there may be other subclasses which
15+
/// are not statically known by godot-rust (e.g. from a script or GDExtension). The fallback branch can either be `_` (discard object), or
16+
/// `_(variable)` to access the original object inside the fallback arm.
17+
///
18+
/// # Example
19+
/// ```no_run
20+
/// # use godot::prelude::*;
21+
/// # use godot_core::classes::{InputEvent, InputEventAction};
22+
/// # fn some_input() -> Gd<InputEvent> { unimplemented!() }
23+
/// # // Hack to keep amount of SELECTED_CLASSES limited:
24+
/// # type InputEventMouseButton = InputEventAction;
25+
/// # type InputEventMouseMotion = InputEventAction;
26+
/// // Basic syntax.
27+
/// let event: Gd<InputEvent> = some_input();
28+
///
29+
/// let simple_dispatch: i32 = match_class!(event, {
30+
/// InputEventMouseButton(btn) => 1,
31+
/// InputEventMouseMotion(motion) => 2,
32+
/// InputEventAction(action) => 3,
33+
/// _ => 0, // Fallback.
34+
/// });
35+
///
36+
/// // More diverse dispatch patterns are also supported.
37+
/// let fancy_dispatch: i32 = match_class!(some_input(), {
38+
/// InputEventMouseButton(btn) => 1,
39+
///
40+
/// // Block syntax for multiple statements:
41+
/// InputEventMouseMotion(motion) => {
42+
/// godot_print!("motion");
43+
/// 2
44+
/// },
45+
///
46+
/// // Qualified types supported:
47+
/// godot::classes::InputEventAction(action) => 3,
48+
///
49+
/// // Fallback with variable -- retrieves original Gd<InputEvent>.
50+
/// // Equivalent to pattern `InputEvent(original)`.
51+
/// _(original) => 0,
52+
/// });
53+
///
54+
/// // event_type is now 0, 1, 2, or 3
55+
/// ```
56+
///
57+
/// # Limitations
58+
/// The expression block is currently wrapped by a closure, so you cannot use control-flow statements like `?`, `return`, `continue`, `break`.
59+
#[macro_export]
60+
// Note: annoyingly shows full implementation in docs. For workarounds, either move impl to a helper macro, or use something like
61+
// https://crates.io/crates/clean-macro-docs.
62+
macro_rules! match_class {
63+
($subject:expr, {
64+
$(
65+
$($class:ident)::+($var:ident) => $body:expr
66+
),+,
67+
_($fallback_var:ident) => $fallback:expr
68+
$(,)?
69+
}) => {
70+
(|| {
71+
let mut __evt = $subject;
72+
$(
73+
__evt = match __evt.try_cast::<$($class)::*>() {
74+
Ok($var) => return $body,
75+
Err(e) => e,
76+
};
77+
)+
78+
let $fallback_var = __evt;
79+
$fallback
80+
})()
81+
};
82+
83+
($subject:expr, {
84+
$(
85+
$($class:ident)::+($var:ident) => $body:expr
86+
),+,
87+
_ => $fallback:expr
88+
$(,)?
89+
}) => {
90+
(|| {
91+
let mut __evt = $subject;
92+
$(
93+
__evt = match __evt.try_cast::<$($class)::*>() {
94+
Ok($var) => return $body,
95+
Err(e) => e,
96+
};
97+
)+
98+
$fallback
99+
})()
100+
};
101+
}

godot-core/src/classes/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818
1919
mod class_runtime;
2020
mod manual_extensions;
21+
mod match_class;
2122

2223
// Re-exports all generated classes, interface traits and sidecar modules.
2324
pub use crate::gen::classes::*;
2425

26+
// Macro re-export.
27+
pub use crate::match_class;
28+
2529
/// Support for Godot _native structures_.
2630
///
2731
/// Native structures are a niche API in Godot. These are low-level data types that are passed as pointers to/from the engine.

godot/src/prelude.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ pub use super::meta::error::{ConvertError, IoError};
1616
pub use super::meta::{FromGodot, GodotConvert, ToGodot};
1717

1818
pub use super::classes::{
19-
INode, INode2D, INode3D, IObject, IPackedScene, IRefCounted, IResource, ISceneTree, Node,
20-
Node2D, Node3D, Object, PackedScene, RefCounted, Resource, SceneTree,
19+
match_class, INode, INode2D, INode3D, IObject, IPackedScene, IRefCounted, IResource,
20+
ISceneTree, Node, Node2D, Node3D, Object, PackedScene, RefCounted, Resource, SceneTree,
2121
};
2222
pub use super::global::{
2323
godot_error, godot_print, godot_print_rich, godot_script_error, godot_warn,
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
use crate::framework::itest;
9+
use godot::prelude::*; // Expect match_class! to be in prelude.
10+
11+
// Ensure static types are as expected.
12+
fn require_object(_: &Object) {}
13+
fn require_node(_: &Node) {}
14+
fn require_node2d(_: &Node2D) {}
15+
16+
#[itest]
17+
fn match_class_basic_dispatch() {
18+
let node2d = Node2D::new_alloc();
19+
let obj: Gd<Object> = node2d.upcast();
20+
let to_free = obj.clone();
21+
22+
let result = match_class!(obj, {
23+
Node2D(node) => {
24+
require_node2d(&node);
25+
1
26+
},
27+
Node(node) => {
28+
require_node(&node);
29+
2
30+
},
31+
_ => 3 // No comma.
32+
});
33+
34+
assert_eq!(result, 1);
35+
to_free.free();
36+
}
37+
38+
#[itest]
39+
fn match_class_shadowed_by_more_general() {
40+
let node2d = Node2D::new_alloc();
41+
let obj: Gd<Object> = node2d.upcast();
42+
let to_free = obj.clone();
43+
44+
let result = match_class!(obj, {
45+
Node(_node) => 1,
46+
Node2D(_node) => 2,
47+
_ => 3, // Comma.
48+
});
49+
50+
assert_eq!(
51+
result, 1,
52+
"Node2D branch never hit, since Node one is more general and first"
53+
);
54+
to_free.free();
55+
}
56+
57+
#[itest]
58+
fn match_class_ignored_fallback() {
59+
let obj: Gd<Object> = RefCounted::new_gd().upcast();
60+
61+
let result = match_class!(obj, {
62+
godot::classes::Node(_node) => 1, // Test qualified types.
63+
Resource(_res) => 2,
64+
_ => 3,
65+
});
66+
67+
assert_eq!(result, 3);
68+
}
69+
70+
#[itest]
71+
fn match_class_named_fallback_matched() {
72+
let obj: Gd<Object> = Resource::new_gd().upcast();
73+
74+
let result = match_class!(obj, {
75+
Node(_node) => 1,
76+
Node2D(_node) => 2,
77+
78+
// Named fallback with access to original object.
79+
_(other) => {
80+
require_object(&other);
81+
assert_eq!(other.get_class(), "Resource".into());
82+
3
83+
}
84+
});
85+
86+
assert_eq!(result, 3);
87+
}
88+
89+
#[itest]
90+
fn match_class_named_fallback_unmatched() {
91+
// Test complex inline expression.
92+
let result = match_class!(Resource::new_gd().upcast::<Object>(), {
93+
Node(_node) => 1,
94+
Resource(_res) => 2,
95+
_(_ignored) => 3,
96+
});
97+
98+
assert_eq!(result, 2);
99+
}

itest/rust/src/engine_tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod codegen_enums_test;
1111
mod codegen_test;
1212
mod engine_enum_test;
1313
mod gfile_test;
14+
mod match_class_test;
1415
mod native_st_niche_audio_test;
1516
mod native_st_niche_pointer_test;
1617
mod native_structures_test;

0 commit comments

Comments
 (0)