diff --git a/CHANGELOG.md b/CHANGELOG.md index f04aef73..b3d61f3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 where E: IntoError` to allow returning custom error types to + Ruby. ### Changed - Closures/Functions used as Ruby blocks/procs take an additional first diff --git a/Cargo.lock b/Cargo.lock index c434bfee..988b9a6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,7 +105,7 @@ dependencies = [ [[package]] name = "magnus" -version = "0.6.0" +version = "0.7.0" dependencies = [ "bytes", "magnus", diff --git a/Cargo.toml b/Cargo.toml index e60023d9..7e123daa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] @@ -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"] @@ -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" diff --git a/src/error.rs b/src/error.rs index 6c53de3a..c201b979 100644 --- a/src/error.rs +++ b/src/error.rs @@ -320,6 +320,19 @@ impl From 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 @@ -372,6 +385,13 @@ impl From 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)] diff --git a/src/method.rs b/src/method.rs index ee77179b..2211c89c 100644 --- a/src/method.rs +++ b/src/method.rs @@ -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, @@ -74,12 +74,15 @@ mod private { fn into_return_value(self) -> Result; } - impl ReturnValue for Result + impl ReturnValue for Result where T: IntoValue, + E: IntoError, { fn into_return_value(self) -> Result { - 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)) } } @@ -88,83 +91,92 @@ mod private { T: IntoValue, { fn into_return_value(self) -> Result { - Ok(self).into_return_value() + Ok::(self).into_return_value() } } - impl ReturnValue for Yield + impl ReturnValue for Result, E> where I: Iterator, T: IntoValue, + E: IntoError, { fn into_return_value(self) -> Result { - 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 ReturnValue for Result, Error> + impl ReturnValue for Yield where I: Iterator, T: IntoValue, { fn into_return_value(self) -> Result { - self?.into_return_value() + Ok::(self).into_return_value() } } - impl ReturnValue for YieldValues + impl ReturnValue for Result, E> where I: Iterator, T: ArgList, + E: IntoError, { fn into_return_value(self) -> Result { - 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 ReturnValue for Result, Error> + impl ReturnValue for YieldValues where I: Iterator, T: ArgList, { fn into_return_value(self) -> Result { - self?.into_return_value() + Ok::(self).into_return_value() } } - impl ReturnValue for YieldSplat + impl ReturnValue for Result, E> where I: Iterator, + E: IntoError, { fn into_return_value(self) -> Result { - 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 ReturnValue for Result, Error> + impl ReturnValue for YieldSplat where I: Iterator, { fn into_return_value(self) -> Result { - self?.into_return_value() + Ok::(self).into_return_value() } } @@ -178,9 +190,12 @@ mod private { } } - impl InitReturn for Result<(), Error> { + impl 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() })) } } diff --git a/tests/return_custom_error.rs b/tests/return_custom_error.rs new file mode 100644 index 00000000..1d330e99 --- /dev/null +++ b/tests/return_custom_error.rs @@ -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""# + ); +}