Skip to content

match_class! macro to dispatch subclasses #1225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions godot-core/src/classes/match_class.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* 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/.
*/

/// Dispatches a class to different subclasses.
///
/// Similar to a `match` statement, but with downcasts. Earlier matches dominate, so keep more-derived classes first.
/// The current implementation checks [`Gd::try_cast()`][crate::obj::Gd::try_cast] linearly with the number of branches.
/// This may change in the future.
///
/// Requires a fallback branch, even if all direct known classes are handled. The reason for this is that there may be other subclasses which
/// are not statically known by godot-rust (e.g. from a script or GDExtension). The fallback branch can either be `_` (discard object), or
/// `_(variable)` to access the original object inside the fallback arm.
///
/// # Example
/// ```no_run
/// # use godot::prelude::*;
/// # use godot_core::classes::{InputEvent, InputEventAction};
/// # fn some_input() -> Gd<InputEvent> { unimplemented!() }
/// # // Hack to keep amount of SELECTED_CLASSES limited:
/// # type InputEventMouseButton = InputEventAction;
/// # type InputEventMouseMotion = InputEventAction;
/// // Basic syntax.
/// let event: Gd<InputEvent> = some_input();
///
/// let simple_dispatch: i32 = match_class!(event, {
/// InputEventMouseButton(btn) => 1,
/// InputEventMouseMotion(motion) => 2,
/// InputEventAction(action) => 3,
/// _ => 0, // Fallback.
/// });
///
/// // More diverse dispatch patterns are also supported.
/// let fancy_dispatch: i32 = match_class!(some_input(), {
/// InputEventMouseButton(btn) => 1,
///
/// // Block syntax for multiple statements:
/// InputEventMouseMotion(motion) => {
/// godot_print!("motion");
/// 2
/// },
///
/// // Qualified types supported:
/// godot::classes::InputEventAction(action) => 3,
///
/// // Fallback with variable -- retrieves original Gd<InputEvent>.
/// // Equivalent to pattern `InputEvent(original)`.
/// _(original) => 0,
/// });
///
/// // event_type is now 0, 1, 2, or 3
/// ```
///
/// # Limitations
/// The expression block is currently wrapped by a closure, so you cannot use control-flow statements like `?`, `return`, `continue`, `break`.
#[macro_export]
// Note: annoyingly shows full implementation in docs. For workarounds, either move impl to a helper macro, or use something like
// https://crates.io/crates/clean-macro-docs.
macro_rules! match_class {
($subject:expr, {
$(
$($class:ident)::+($var:ident) => $body:expr
),+,
_($fallback_var:ident) => $fallback:expr
$(,)?
}) => {
(|| {
let mut __evt = $subject;
$(
__evt = match __evt.try_cast::<$($class)::*>() {
Ok($var) => return $body,
Err(e) => e,
};
)+
let $fallback_var = __evt;
$fallback
})()
};

($subject:expr, {
$(
$($class:ident)::+($var:ident) => $body:expr
),+,
_ => $fallback:expr
$(,)?
}) => {
(|| {
let mut __evt = $subject;
$(
__evt = match __evt.try_cast::<$($class)::*>() {
Ok($var) => return $body,
Err(e) => e,
};
)+
$fallback
})()
};
}
4 changes: 4 additions & 0 deletions godot-core/src/classes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@

mod class_runtime;
mod manual_extensions;
mod match_class;

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

// Macro re-export.
pub use crate::match_class;

/// Support for Godot _native structures_.
///
/// Native structures are a niche API in Godot. These are low-level data types that are passed as pointers to/from the engine.
Expand Down
4 changes: 2 additions & 2 deletions godot/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ pub use super::meta::error::{ConvertError, IoError};
pub use super::meta::{FromGodot, GodotConvert, ToGodot};

pub use super::classes::{
INode, INode2D, INode3D, IObject, IPackedScene, IRefCounted, IResource, ISceneTree, Node,
Node2D, Node3D, Object, PackedScene, RefCounted, Resource, SceneTree,
match_class, INode, INode2D, INode3D, IObject, IPackedScene, IRefCounted, IResource,
ISceneTree, Node, Node2D, Node3D, Object, PackedScene, RefCounted, Resource, SceneTree,
};
pub use super::global::{
godot_error, godot_print, godot_print_rich, godot_script_error, godot_warn,
Expand Down
99 changes: 99 additions & 0 deletions itest/rust/src/engine_tests/match_class_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright (c) godot-rust; Bromeon and contributors.
* 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 crate::framework::itest;
use godot::prelude::*; // Expect match_class! to be in prelude.

// Ensure static types are as expected.
fn require_object(_: &Object) {}
fn require_node(_: &Node) {}
fn require_node2d(_: &Node2D) {}

#[itest]
fn match_class_basic_dispatch() {
let node2d = Node2D::new_alloc();
let obj: Gd<Object> = node2d.upcast();
let to_free = obj.clone();

let result = match_class!(obj, {
Node2D(node) => {
require_node2d(&node);
1
},
Node(node) => {
require_node(&node);
2
},
_ => 3 // No comma.
});

assert_eq!(result, 1);
to_free.free();
}

#[itest]
fn match_class_shadowed_by_more_general() {
let node2d = Node2D::new_alloc();
let obj: Gd<Object> = node2d.upcast();
let to_free = obj.clone();

let result = match_class!(obj, {
Node(_node) => 1,
Node2D(_node) => 2,
_ => 3, // Comma.
});

assert_eq!(
result, 1,
"Node2D branch never hit, since Node one is more general and first"
);
to_free.free();
}

#[itest]
fn match_class_ignored_fallback() {
let obj: Gd<Object> = RefCounted::new_gd().upcast();

let result = match_class!(obj, {
godot::classes::Node(_node) => 1, // Test qualified types.
Resource(_res) => 2,
_ => 3,
});

assert_eq!(result, 3);
}

#[itest]
fn match_class_named_fallback_matched() {
let obj: Gd<Object> = Resource::new_gd().upcast();

let result = match_class!(obj, {
Node(_node) => 1,
Node2D(_node) => 2,

// Named fallback with access to original object.
_(other) => {
require_object(&other);
assert_eq!(other.get_class(), "Resource".into());
3
}
});

assert_eq!(result, 3);
}

#[itest]
fn match_class_named_fallback_unmatched() {
// Test complex inline expression.
let result = match_class!(Resource::new_gd().upcast::<Object>(), {
Node(_node) => 1,
Resource(_res) => 2,
_(_ignored) => 3,
});

assert_eq!(result, 2);
}
1 change: 1 addition & 0 deletions itest/rust/src/engine_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod codegen_enums_test;
mod codegen_test;
mod engine_enum_test;
mod gfile_test;
mod match_class_test;
mod native_st_niche_audio_test;
mod native_st_niche_pointer_test;
mod native_structures_test;
Expand Down
Loading