Skip to content

Commit

Permalink
allow returning custom error types with automatic conversion to Error
Browse files Browse the repository at this point in the history
  • Loading branch information
matsadler committed Apr 3, 2024
1 parent b7bf354 commit 6277554
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 33 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
- `Ruby::waitpid`.
- `RHash::lookup2`.
- `Ruby::define_data` new for Ruby 3.3.
- `IntoError` trait for conversion to `Error`, plus `impl ReturnValue for
Result<T, E> where E: IntoError` to allow returning custom error types to
Ruby.

### Changed
- Closures/Functions used as Ruby blocks/procs take an additional first
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 20 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "magnus"
version = "0.6.0"
version = "0.7.0"
edition = "2021"
description = "High level Ruby bindings. Write Ruby extension gems in Rust, or call Ruby code from a Rust binary."
keywords = ["ruby", "rubygem", "extension", "gem"]
Expand All @@ -13,7 +13,12 @@ exclude = [".github", ".gitignore"]

[workspace]
members = ["magnus-macros"]
exclude = ["examples/rust_blank/ext/rust_blank", "examples/custom_exception_ruby/ext/ahriman", "examples/custom_exception_rust/ext/ahriman", "examples/complete_object/ext/temperature"]
exclude = [
"examples/rust_blank/ext/rust_blank",
"examples/custom_exception_ruby/ext/ahriman",
"examples/custom_exception_rust/ext/ahriman",
"examples/complete_object/ext/temperature",
]

[features]
default = ["old-api"]
Expand All @@ -25,12 +30,22 @@ rb-sys = []
[dependencies]
bytes = { version = "1", optional = true }
magnus-macros = { version = "0.6.0", path = "magnus-macros" }
rb-sys = { version = "0.9.85", default-features = false, features = ["bindgen-rbimpls", "bindgen-deprecated-types", "stable-api"] }
rb-sys = { version = "0.9.85", default-features = false, features = [
"bindgen-rbimpls",
"bindgen-deprecated-types",
"stable-api",
] }
seq-macro = "0.3"

[dev-dependencies]
magnus = { path = ".", features = ["embed", "rb-sys", "bytes"] }
rb-sys = { version = "0.9", default-features = false, features = ["stable-api-compiled-fallback"] }
magnus = { path = ".", default-features = false, features = [
"embed",
"rb-sys",
"bytes",
] }
rb-sys = { version = "0.9", default-features = false, features = [
"stable-api-compiled-fallback",
] }

[build-dependencies]
rb-sys-env = "0.1.2"
Expand Down
20 changes: 20 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,19 @@ impl From<Exception> for Error {
}
}

/// Conversions into [`Error`].
pub trait IntoError {
/// Convert `self` into [`Error`].
fn into_error(self, ruby: &Ruby) -> Error;
}

impl IntoError for Error {
#[inline]
fn into_error(self, _: &Ruby) -> Error {
self
}
}

/// A wrapper to make a [`Error`] [`Send`] + [`Sync`].
///
/// [`Error`] is not [`Send`] or [`Sync`] as it provides a way to call some of
Expand Down Expand Up @@ -372,6 +385,13 @@ impl From<Error> for OpaqueError {
}
}

impl IntoError for OpaqueError {
#[inline]
fn into_error(self, _: &Ruby) -> Error {
Error(self.0)
}
}

/// The state of a call to Ruby exiting early, interrupting the normal flow
/// of code.
#[derive(Debug)]
Expand Down
69 changes: 42 additions & 27 deletions src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::{
do_yield_iter, do_yield_splat_iter, do_yield_values_iter, Proc, Yield, YieldSplat,
YieldValues,
},
error::{raise, Error},
error::{raise, Error, IntoError},
into_value::{ArgList, IntoValue},
r_array::RArray,
try_convert::TryConvert,
Expand Down Expand Up @@ -74,12 +74,15 @@ mod private {
fn into_return_value(self) -> Result<Value, Error>;
}

impl<T> ReturnValue for Result<T, Error>
impl<T, E> ReturnValue for Result<T, E>
where
T: IntoValue,
E: IntoError,
{
fn into_return_value(self) -> Result<Value, Error> {
self.map(|val| unsafe { val.into_value_unchecked() })
let ruby = unsafe { Ruby::get_unchecked() };
self.map(|val| val.into_value_with(&ruby))
.map_err(|err| err.into_error(&ruby))
}
}

Expand All @@ -88,83 +91,92 @@ mod private {
T: IntoValue,
{
fn into_return_value(self) -> Result<Value, Error> {
Ok(self).into_return_value()
Ok::<T, Error>(self).into_return_value()
}
}

impl<I, T> ReturnValue for Yield<I>
impl<I, T, E> ReturnValue for Result<Yield<I>, E>
where
I: Iterator<Item = T>,
T: IntoValue,
E: IntoError,
{
fn into_return_value(self) -> Result<Value, Error> {
match self {
let ruby = unsafe { Ruby::get_unchecked() };
self.map(|i| match i {
Yield::Iter(iter) => unsafe {
do_yield_iter(iter);
Ok(Ruby::get_unchecked().qnil().as_value())
ruby.qnil().as_value()
},
Yield::Enumerator(e) => Ok(unsafe { e.into_value_unchecked() }),
}
Yield::Enumerator(e) => e.into_value_with(&ruby),
})
.map_err(|err| err.into_error(&ruby))
}
}

impl<I, T> ReturnValue for Result<Yield<I>, Error>
impl<I, T> ReturnValue for Yield<I>
where
I: Iterator<Item = T>,
T: IntoValue,
{
fn into_return_value(self) -> Result<Value, Error> {
self?.into_return_value()
Ok::<Self, Error>(self).into_return_value()
}
}

impl<I, T> ReturnValue for YieldValues<I>
impl<I, T, E> ReturnValue for Result<YieldValues<I>, E>
where
I: Iterator<Item = T>,
T: ArgList,
E: IntoError,
{
fn into_return_value(self) -> Result<Value, Error> {
match self {
let ruby = unsafe { Ruby::get_unchecked() };
self.map(|i| match i {
YieldValues::Iter(iter) => unsafe {
do_yield_values_iter(iter);
Ok(Ruby::get_unchecked().qnil().as_value())
ruby.qnil().as_value()
},
YieldValues::Enumerator(e) => Ok(unsafe { e.into_value_unchecked() }),
}
YieldValues::Enumerator(e) => e.into_value_with(&ruby),
})
.map_err(|err| err.into_error(&ruby))
}
}

impl<I, T> ReturnValue for Result<YieldValues<I>, Error>
impl<I, T> ReturnValue for YieldValues<I>
where
I: Iterator<Item = T>,
T: ArgList,
{
fn into_return_value(self) -> Result<Value, Error> {
self?.into_return_value()
Ok::<Self, Error>(self).into_return_value()
}
}

impl<I> ReturnValue for YieldSplat<I>
impl<I, E> ReturnValue for Result<YieldSplat<I>, E>
where
I: Iterator<Item = RArray>,
E: IntoError,
{
fn into_return_value(self) -> Result<Value, Error> {
match self {
let ruby = unsafe { Ruby::get_unchecked() };
self.map(|i| match i {
YieldSplat::Iter(iter) => unsafe {
do_yield_splat_iter(iter);
Ok(Ruby::get_unchecked().qnil().as_value())
ruby.qnil().as_value()
},
YieldSplat::Enumerator(e) => Ok(unsafe { e.into_value_unchecked() }),
}
YieldSplat::Enumerator(e) => e.into_value_with(&ruby),
})
.map_err(|err| err.into_error(&ruby))
}
}

impl<I> ReturnValue for Result<YieldSplat<I>, Error>
impl<I> ReturnValue for YieldSplat<I>
where
I: Iterator<Item = RArray>,
{
fn into_return_value(self) -> Result<Value, Error> {
self?.into_return_value()
Ok::<Self, Error>(self).into_return_value()
}
}

Expand All @@ -178,9 +190,12 @@ mod private {
}
}

impl InitReturn for Result<(), Error> {
impl<E> InitReturn for Result<(), E>
where
E: IntoError,
{
fn into_init_return(self) -> Result<(), Error> {
self
self.map_err(|err| err.into_error(&unsafe { Ruby::get_unchecked() }))
}
}

Expand Down
28 changes: 28 additions & 0 deletions tests/return_custom_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use magnus::{error::IntoError, function, rb_assert, Error, Ruby};

struct CustomError(&'static str);

impl IntoError for CustomError {
fn into_error(self, ruby: &Ruby) -> Error {
Error::new(
ruby.exception_runtime_error(),
format!("Custom error: {}", self.0),
)
}
}

fn example() -> Result<(), CustomError> {
Err(CustomError("test"))
}

#[test]
fn it_can_bind_function_returning_custom_error() {
let ruby = unsafe { magnus::embed::init() };

ruby.define_global_function("example", function!(example, 0));

rb_assert!(
ruby,
r#"(example rescue $!).message == "Custom error: test""#
);
}

0 comments on commit 6277554

Please sign in to comment.