Skip to content

feat(ecs): add Assume and Unpack traits for Result conversion #17739

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

Closed
wants to merge 7 commits into from
Closed
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
36 changes: 36 additions & 0 deletions crates/bevy_ecs/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator>
//!
//! 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::<MyComponent>(Entity::PLACEHOLDER).unpack()?;
//!
//! world.get::<MyComponent>(Entity::PLACEHOLDER).assume("MyComponent exists")?;
//!
//! Ok(())
//! }
//!
//! # bevy_ecs::system::assert_is_system(my_system);
//! ```
//!
//! [`Schedule`]: crate::schedule::Schedule
//! [`panic`]: panic()
//! [`World`]: crate::world::World
Expand All @@ -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<dyn core::error::Error + Send + Sync + 'static>;

Expand Down
66 changes: 66 additions & 0 deletions crates/bevy_ecs/src/result/assume.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use super::Error;

/// Assume that `Self<T>` 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<T> {
/// 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<T>` to a `Result<T, Self::Error>`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These docs will not be very helpful on mouse-over tooltips.

fn assume<E: Into<Self::Error>>(self, err: E) -> Result<T, Self::Error>;
}

impl<T> Assume<T> for Option<T> {
type Error = Error;

fn assume<E: Into<Self::Error>>(self, err: E) -> Result<T, Self::Error> {
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<i32> = 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<i32> = 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<i32> = None;
let err = value.assume(MyError).unwrap_err();
assert_eq!(err.to_string(), "My custom error");
}
}
56 changes: 56 additions & 0 deletions crates/bevy_ecs/src/result/unpack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use core::{error::Error, fmt};

/// Unpack `Self<T>` 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<T> {
/// 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<T>` to a `Result<T, Self::Error>`.
fn unpack(self) -> Result<T, Self::Error>;
}

impl<T> Unpack<T> for Option<T> {
type Error = NoneError;

fn unpack(self) -> Result<T, Self::Error> {
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<i32> = Some(10);
assert_eq!(value.unpack(), Ok(10));
}

#[test]
fn test_unpack_none() {
let value: Option<i32> = None;
let err = value.unpack().unwrap_err();
assert_eq!(err.to_string(), "Unexpected None value.");
}
}
Loading