diff --git a/crates/bevy_ecs/src/result.rs b/crates/bevy_ecs/src/result.rs index ef5eace90b07e..12bfe37a06166 100644 --- a/crates/bevy_ecs/src/result.rs +++ b/crates/bevy_ecs/src/result.rs @@ -61,6 +61,36 @@ //! If you need special handling of individual fallible systems, you can use Bevy's [`system piping //! feature`] to capture the `Result` output of the system and handle it accordingly. //! +//! # Conveniently returning `Result` +//! +//! The [`Unpack`] and [`Assume`] traits can be used to transform any value into a [`Result`] that +//! can be handled by Rust's `?` operator. This makes working with fallible systems more ergonomic. +//! +//! [`Unpack::unpack`] should be used in place of `unwrap` (to quickly get a value), while +//! [`Assume::assume`] should be used in place of `expect` (if you want a custom error explaining +//! the assumption that was violated). +//! +//! See: +//! +//! By default, these traits are implemented for [`Option`]: +//! +//! ```rust +//! # use bevy_ecs::prelude::*; +//! # #[derive(Component)] +//! # struct MyComponent; +//! use bevy_ecs::result::{Assume, Unpack}; +//! +//! fn my_system(world: &World) -> Result { +//! world.get::(Entity::PLACEHOLDER).unpack()?; +//! +//! world.get::(Entity::PLACEHOLDER).assume("MyComponent exists")?; +//! +//! Ok(()) +//! } +//! +//! # bevy_ecs::system::assert_is_system(my_system); +//! ``` +//! //! [`Schedule`]: crate::schedule::Schedule //! [`panic`]: panic() //! [`World`]: crate::world::World @@ -70,9 +100,15 @@ //! [`App::set_system_error_handler`]: ../../bevy_app/struct.App.html#method.set_system_error_handler //! [`system piping feature`]: crate::system::In +mod assume; +mod unpack; + use crate::{component::Tick, resource::Resource}; use alloc::{borrow::Cow, boxed::Box}; +pub use assume::Assume; +pub use unpack::Unpack; + /// A dynamic error type for use in fallible systems. pub type Error = Box; diff --git a/crates/bevy_ecs/src/result/assume.rs b/crates/bevy_ecs/src/result/assume.rs new file mode 100644 index 0000000000000..fa429fa6c618d --- /dev/null +++ b/crates/bevy_ecs/src/result/assume.rs @@ -0,0 +1,66 @@ +use super::Error; + +/// Assume that `Self` is `T`, otherwise return the provided error. +/// +/// This can be a drop-in replacement for `expect`, combined with the question mark operator and +/// [`Result`](super::Result) return type, to get the same ergonomics as `expect` but without the +/// panicking behavior (when using a non-panicking error handler). +pub trait Assume { + /// The error type returned by [`Assume::assume`]. + /// + /// Typically implements the [`Error`] trait, allowing it to match Bevy's fallible system + /// [`Result`](super::Result) return type. + type Error; + + /// Convert `Self` to a `Result`. + fn assume>(self, err: E) -> Result; +} + +impl Assume for Option { + type Error = Error; + + fn assume>(self, err: E) -> Result { + self.ok_or_else(|| err.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::std::string::ToString; + use core::{error::Error, fmt}; + + #[test] + fn test_assume_some() { + let value: Option = Some(20); + + match value.assume("Error message") { + Ok(value) => assert_eq!(value, 20), + Err(err) => panic!("Unexpected error: {err}"), + } + } + + #[test] + fn test_assume_none_with_str() { + let value: Option = None; + let err = value.assume("index 1 should exist").unwrap_err(); + assert_eq!(err.to_string(), "index 1 should exist"); + } + + #[test] + fn test_assume_none_with_custom_error() { + #[derive(Debug)] + struct MyError; + + impl fmt::Display for MyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "My custom error") + } + } + impl Error for MyError {} + + let value: Option = None; + let err = value.assume(MyError).unwrap_err(); + assert_eq!(err.to_string(), "My custom error"); + } +} diff --git a/crates/bevy_ecs/src/result/unpack.rs b/crates/bevy_ecs/src/result/unpack.rs new file mode 100644 index 0000000000000..272f23976dce2 --- /dev/null +++ b/crates/bevy_ecs/src/result/unpack.rs @@ -0,0 +1,56 @@ +use core::{error::Error, fmt}; + +/// Unpack `Self` to `T`, otherwise return [`Unpack::Error`]. +/// +/// This can be a drop-in replacement for `unwrap`, combined with the question mark operator and +/// [`Result`](super::Result) return type, to get the same ergonomics as `unwrap` but without the +/// panicking behavior (when using a non-panicking error handler). +pub trait Unpack { + /// The error type returned by [`Unpack::unpack`]. + /// + /// Typically implements the [`Error`] trait, allowing it to match Bevy's fallible system + /// [`Result`](super::Result) return type. + type Error; + + /// Convert `Self` to a `Result`. + fn unpack(self) -> Result; +} + +impl Unpack for Option { + type Error = NoneError; + + fn unpack(self) -> Result { + self.ok_or(NoneError) + } +} + +/// An [`Error`] which indicates that an [`Option`] was [`None`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NoneError; + +impl fmt::Display for NoneError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unexpected None value.") + } +} + +impl Error for NoneError {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::std::string::ToString; + + #[test] + fn test_unpack_some() { + let value: Option = Some(10); + assert_eq!(value.unpack(), Ok(10)); + } + + #[test] + fn test_unpack_none() { + let value: Option = None; + let err = value.unpack().unwrap_err(); + assert_eq!(err.to_string(), "Unexpected None value."); + } +}