Skip to content

Commit 8a3f1fd

Browse files
committed
Add func_bail! macro
1 parent ec4b4fa commit 8a3f1fd

6 files changed

Lines changed: 55 additions & 45 deletions

File tree

godot-core/src/meta/error/error_to_godot.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::meta::{GodotConvert, ToGodot};
2424
/// One option is to make the GDScript function call fail, meaning the GDScript function will be aborted.
2525
/// This happens when you use `String` error type. The declared type on GDScript side is `T`.
2626
///
27-
/// For a more ergonomic catch-all approach, use [`FuncError`] / [`FuncResult`] which lets you use `?` with any `impl Error` type.
27+
/// For a more ergonomic catch-all approach, use [`FuncError`] which lets you use `?` with any `impl Error` type.
2828
///
2929
/// ```no_run
3030
/// # use godot::prelude::*;
@@ -108,6 +108,8 @@ use crate::meta::{GodotConvert, ToGodot};
108108
/// | `Array` with 0/1 elems | `Array<T>` | `[val]` | `[]` | `a.is_empty()`<br>`a.front()` -- typed! |
109109
/// | custom `RustResult` | `Gd<RustResult>` | wrap in class | wrap in class | `r.is_ok()`<br>`r.unwrap()` |
110110
///
111+
/// [`FuncError`]: crate::meta::error::FuncError
112+
/// [`func_bail!`]: crate::meta::error::func_bail
111113
pub trait ErrorToGodot<T: ToGodot>: Sized {
112114
/// The type to which `Result<T, Self>` is mapped on Godot side.
113115
type Mapped: GodotConvert;

godot-core/src/meta/error/func_error.rs

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use std::error::Error;
1111
use std::fmt;
1212

13-
use crate::builtin::CowStr;
1413
use crate::meta::ToGodot;
1514
use crate::meta::error::ErrorToGodot;
1615

@@ -22,16 +21,16 @@ use crate::meta::error::ErrorToGodot;
2221
/// Its main advantage over other [`ErrorToGodot`] types is that it preserves type safety: GDScript's static analysis sees the return type `T`.
2322
/// This is the (more common) happy path, returned from Rust as `Ok(T)`.
2423
///
25-
/// `FuncError` enables automatic conversions from other errors via `?` operator. This means you can mix different error types within the same
26-
/// function body -- each one just propagates via `?` and its message is forwarded to Godot. `String` and `&str` also convert, for cases where
27-
/// you want to fail with a direct message.
24+
/// `FuncError` enables automatic conversions from other errors via `?` operator. This means you can mix different error types within the same
25+
/// function body -- each one propagates via `?` and its message is forwarded to Godot. `String` and `&str` also convert, for cases where you
26+
/// want to fail with a direct message.
2827
///
29-
/// Use [`FuncResult<T>`] as a convenient alias for `Result<T, FuncError>`.
28+
/// Use [`FuncResult<T>`] as an alias for `Result<T, FuncError>`, and the [`func_bail!`] macro for early returns with an error message.
3029
///
3130
/// # Example
3231
/// ```no_run
3332
/// # use godot::prelude::*;
34-
/// use godot::meta::error::FuncResult;
33+
/// use godot::meta::error::{FuncResult, func_bail};
3534
/// # #[derive(GodotClass)] #[class(init, base=Node)] struct PlayerData;
3635
///
3736
/// #[godot_api]
@@ -44,9 +43,8 @@ use crate::meta::error::ErrorToGodot;
4443
/// let text = std::fs::read_to_string(save_path.to_string())?;
4544
/// let score = text.trim().parse::<i64>()?;
4645
///
47-
/// // Strings are also supported via From trait.
4846
/// if score < 0 {
49-
/// return Err(FuncError::from_message("Corrupted save file: high score is negative."));
47+
/// func_bail!("Corrupted save file {save_path}: high score is negative.");
5048
/// }
5149
///
5250
/// Ok(score)
@@ -80,18 +78,6 @@ impl FuncError {
8078
}
8179
}
8280

83-
/// Create a `FuncError` from a plain message string.
84-
///
85-
/// Accepts `&'static str` (no allocation) or `String` / `format!(...)` output. For non-static string slices, pass `.to_string()` or
86-
/// use `format!()`. Useful when you want to fail a `#[func]` call with a plain message and have no underlying error type to propagate.
87-
pub fn from_message(msg: impl Into<CowStr>) -> Self {
88-
Self {
89-
inner: Box::new(MessageError {
90-
message: msg.into(),
91-
}),
92-
}
93-
}
94-
9581
/// Attempt to downcast the inner error to a concrete type.
9682
pub fn downcast_ref<E: Error + 'static>(&self) -> Option<&E> {
9783
self.inner.downcast_ref::<E>()
@@ -120,7 +106,7 @@ impl fmt::Debug for FuncError {
120106
/// |---|---|
121107
/// | Any `E: Error + Send + Sync + 'static` | Boxed directly; covers `std::io::Error`, `ParseIntError`, and any custom error type |
122108
/// | `Box<dyn Error + Send + Sync + 'static>` | Used as-is |
123-
/// | `String` | Wrapped in a message-only error (`StringError` in stdlib) |
109+
/// | `String` | Wrapped in a message-only error |
124110
/// | `&str` (any lifetime) | Copied to `String`, then wrapped — the lifetime is not propagated |
125111
impl<E: Into<Box<dyn Error + Send + Sync + 'static>>> From<E> for FuncError {
126112
fn from(err: E) -> Self {
@@ -152,24 +138,27 @@ impl<T: ToGodot<Via: Clone>> ErrorToGodot<T> for FuncError {
152138
pub type FuncResult<T> = Result<T, FuncError>;
153139

154140
// ----------------------------------------------------------------------------------------------------------------------------------------------
155-
// Internal helper: wraps a plain message string as an error
141+
// Macro for immediately exiting function.
156142

157-
#[derive(Debug)]
158-
struct MessageError {
159-
message: CowStr,
160-
}
161-
162-
impl fmt::Display for MessageError {
163-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164-
f.write_str(&self.message)
165-
}
143+
/// Return early from a `#[func]`, creating a [`FuncError`] from an error message.
144+
///
145+
/// Same principle as [`eyre::bail!`](https://docs.rs/eyre/latest/eyre/macro.bail.html),
146+
/// [`miette::bail!`](https://docs.rs/miette/latest/miette/macro.bail.html), and
147+
/// [`anyhow::bail!`](https://docs.rs/anyhow/latest/anyhow/macro.bail.html).
148+
///
149+
/// This macro expands to `return Err(FuncError::from(...))`.
150+
/// Accepts a string literal or a `format!`-style format string with arguments. See [`FuncError`] for usage example.
151+
#[macro_export]
152+
macro_rules! func_bail {
153+
($($arg:tt)*) => {
154+
return ::std::result::Result::Err($crate::meta::error::FuncError::from(
155+
::std::format!($($arg)*)
156+
))
157+
};
166158
}
167159

168-
impl Error for MessageError {}
169-
170160
// ----------------------------------------------------------------------------------------------------------------------------------------------
171161

172-
// Verify the From impls documented on the blanket From impl.
173162
#[cfg(test)]
174163
mod tests {
175164
use super::*;
@@ -179,7 +168,7 @@ mod tests {
179168

180169
#[test]
181170
fn from_concrete_error() {
182-
// Any E: Error + Send + Sync + 'static -- here using ParseIntError.
171+
// Accepts any E: Error + Send + Sync + 'static -- here ParseIntError.
183172
let err: Result<i32, _> = "x".parse();
184173
assert_func_error(err.unwrap_err().into());
185174
}
@@ -221,9 +210,21 @@ mod tests {
221210
}
222211

223212
#[test]
224-
fn from_message_constructor() {
225-
let e = FuncError::from_message(format!("value was {}", 42));
226-
assert_eq!(e.to_string(), "value was 42");
213+
fn macro_literal_returns_early() {
214+
fn run() -> FuncResult<i32> {
215+
func_bail!("literal message");
216+
}
217+
let err = run().unwrap_err();
218+
assert_eq!(err.to_string(), "literal message");
219+
}
220+
221+
#[test]
222+
fn macro_format_returns_early() {
223+
fn run(x: i32) -> FuncResult<i32> {
224+
func_bail!("value was {x}");
225+
}
226+
let err = run(42).unwrap_err();
227+
assert_eq!(err.to_string(), "value was 42");
227228
}
228229

229230
#[test]
@@ -235,7 +236,7 @@ mod tests {
235236

236237
#[test]
237238
fn downcast_ref_wrong_type_returns_none() {
238-
let e: FuncError = "not an io error".into();
239+
let e: FuncError = "not an io::Error".into();
239240
assert!(e.downcast_ref::<std::io::Error>().is_none());
240241
}
241242
}

godot-core/src/meta/error/io_error.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ use crate::classes::FileAccess;
1212
use crate::global::Error as GodotError;
1313
use crate::obj::Gd;
1414

15-
/// Error that can occur while using `gdext` IO utilities.
15+
/// Error that can occur while using godot-rust I/O utilities.
16+
///
17+
/// Some APIs using this:
18+
/// - [`tools::try_load()`][crate::tools::try_load]
19+
/// - [`tools::try_save()`][crate::tools::try_save]
20+
/// - [`tools::GFile::try_from_unique()`][crate::tools::GFile::try_from_unique]
1621
#[derive(Debug)]
1722
pub struct IoError {
1823
data: ErrorData,

godot-core/src/meta/error/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ pub use error_to_godot::*;
2222
pub use func_error::*;
2323
pub use io_error::*;
2424
pub use string_error::*;
25+
26+
pub use crate::func_bail;

godot/src/prelude.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub use super::global::{
1414
godot_error, godot_print, godot_print_rich, godot_script_error, godot_warn,
1515
};
1616
pub use super::init::{ExtensionLibrary, InitLevel, InitStage, gdextension};
17-
pub use super::meta::error::{ConvertError, FuncError, FuncResult};
17+
pub use super::meta::error::ConvertError; // Possibly add later: FuncError, FuncResult, func_bail
1818
pub use super::meta::{FromGodot, GodotConvert, ToGodot};
1919
pub use super::obj::{
2020
AsDyn, Base, DynGd, DynGdMut, DynGdRef, Gd, GdMut, GdRef, GodotClass, Inherits, InstanceId,

itest/rust/src/register_tests/func_result_test.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
use godot::builtin::VariantType;
99
use godot::global::Error;
10-
use godot::meta::error::{CallError, FuncError, FuncResult};
10+
use godot::meta::error::{CallError, FuncResult, func_bail};
1111
use godot::prelude::*;
1212

1313
use crate::framework::itest;
@@ -61,7 +61,7 @@ impl FuncResulter {
6161

6262
#[func]
6363
fn func_result_err_message(&self) -> FuncResult<i64> {
64-
Err(FuncError::from_message("custom message"))
64+
func_bail!("custom message");
6565
}
6666

6767
#[func]
@@ -78,7 +78,7 @@ impl FuncResulter {
7878

7979
#[func]
8080
fn func_result_err_str_literal(&self) -> FuncResult<i64> {
81-
Err(FuncError::from_message("save file is corrupted"))
81+
func_bail!("save file is corrupted");
8282
}
8383

8484
#[func]

0 commit comments

Comments
 (0)