Skip to content

Commit 6277554

Browse files
committed
allow returning custom error types with automatic conversion to Error
1 parent b7bf354 commit 6277554

File tree

6 files changed

+114
-33
lines changed

6 files changed

+114
-33
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
- `Ruby::waitpid`.
2323
- `RHash::lookup2`.
2424
- `Ruby::define_data` new for Ruby 3.3.
25+
- `IntoError` trait for conversion to `Error`, plus `impl ReturnValue for
26+
Result<T, E> where E: IntoError` to allow returning custom error types to
27+
Ruby.
2528

2629
### Changed
2730
- Closures/Functions used as Ruby blocks/procs take an additional first

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "magnus"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
edition = "2021"
55
description = "High level Ruby bindings. Write Ruby extension gems in Rust, or call Ruby code from a Rust binary."
66
keywords = ["ruby", "rubygem", "extension", "gem"]
@@ -13,7 +13,12 @@ exclude = [".github", ".gitignore"]
1313

1414
[workspace]
1515
members = ["magnus-macros"]
16-
exclude = ["examples/rust_blank/ext/rust_blank", "examples/custom_exception_ruby/ext/ahriman", "examples/custom_exception_rust/ext/ahriman", "examples/complete_object/ext/temperature"]
16+
exclude = [
17+
"examples/rust_blank/ext/rust_blank",
18+
"examples/custom_exception_ruby/ext/ahriman",
19+
"examples/custom_exception_rust/ext/ahriman",
20+
"examples/complete_object/ext/temperature",
21+
]
1722

1823
[features]
1924
default = ["old-api"]
@@ -25,12 +30,22 @@ rb-sys = []
2530
[dependencies]
2631
bytes = { version = "1", optional = true }
2732
magnus-macros = { version = "0.6.0", path = "magnus-macros" }
28-
rb-sys = { version = "0.9.85", default-features = false, features = ["bindgen-rbimpls", "bindgen-deprecated-types", "stable-api"] }
33+
rb-sys = { version = "0.9.85", default-features = false, features = [
34+
"bindgen-rbimpls",
35+
"bindgen-deprecated-types",
36+
"stable-api",
37+
] }
2938
seq-macro = "0.3"
3039

3140
[dev-dependencies]
32-
magnus = { path = ".", features = ["embed", "rb-sys", "bytes"] }
33-
rb-sys = { version = "0.9", default-features = false, features = ["stable-api-compiled-fallback"] }
41+
magnus = { path = ".", default-features = false, features = [
42+
"embed",
43+
"rb-sys",
44+
"bytes",
45+
] }
46+
rb-sys = { version = "0.9", default-features = false, features = [
47+
"stable-api-compiled-fallback",
48+
] }
3449

3550
[build-dependencies]
3651
rb-sys-env = "0.1.2"

src/error.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,19 @@ impl From<Exception> for Error {
320320
}
321321
}
322322

323+
/// Conversions into [`Error`].
324+
pub trait IntoError {
325+
/// Convert `self` into [`Error`].
326+
fn into_error(self, ruby: &Ruby) -> Error;
327+
}
328+
329+
impl IntoError for Error {
330+
#[inline]
331+
fn into_error(self, _: &Ruby) -> Error {
332+
self
333+
}
334+
}
335+
323336
/// A wrapper to make a [`Error`] [`Send`] + [`Sync`].
324337
///
325338
/// [`Error`] is not [`Send`] or [`Sync`] as it provides a way to call some of
@@ -372,6 +385,13 @@ impl From<Error> for OpaqueError {
372385
}
373386
}
374387

388+
impl IntoError for OpaqueError {
389+
#[inline]
390+
fn into_error(self, _: &Ruby) -> Error {
391+
Error(self.0)
392+
}
393+
}
394+
375395
/// The state of a call to Ruby exiting early, interrupting the normal flow
376396
/// of code.
377397
#[derive(Debug)]

src/method.rs

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::{
1313
do_yield_iter, do_yield_splat_iter, do_yield_values_iter, Proc, Yield, YieldSplat,
1414
YieldValues,
1515
},
16-
error::{raise, Error},
16+
error::{raise, Error, IntoError},
1717
into_value::{ArgList, IntoValue},
1818
r_array::RArray,
1919
try_convert::TryConvert,
@@ -74,12 +74,15 @@ mod private {
7474
fn into_return_value(self) -> Result<Value, Error>;
7575
}
7676

77-
impl<T> ReturnValue for Result<T, Error>
77+
impl<T, E> ReturnValue for Result<T, E>
7878
where
7979
T: IntoValue,
80+
E: IntoError,
8081
{
8182
fn into_return_value(self) -> Result<Value, Error> {
82-
self.map(|val| unsafe { val.into_value_unchecked() })
83+
let ruby = unsafe { Ruby::get_unchecked() };
84+
self.map(|val| val.into_value_with(&ruby))
85+
.map_err(|err| err.into_error(&ruby))
8386
}
8487
}
8588

@@ -88,83 +91,92 @@ mod private {
8891
T: IntoValue,
8992
{
9093
fn into_return_value(self) -> Result<Value, Error> {
91-
Ok(self).into_return_value()
94+
Ok::<T, Error>(self).into_return_value()
9295
}
9396
}
9497

95-
impl<I, T> ReturnValue for Yield<I>
98+
impl<I, T, E> ReturnValue for Result<Yield<I>, E>
9699
where
97100
I: Iterator<Item = T>,
98101
T: IntoValue,
102+
E: IntoError,
99103
{
100104
fn into_return_value(self) -> Result<Value, Error> {
101-
match self {
105+
let ruby = unsafe { Ruby::get_unchecked() };
106+
self.map(|i| match i {
102107
Yield::Iter(iter) => unsafe {
103108
do_yield_iter(iter);
104-
Ok(Ruby::get_unchecked().qnil().as_value())
109+
ruby.qnil().as_value()
105110
},
106-
Yield::Enumerator(e) => Ok(unsafe { e.into_value_unchecked() }),
107-
}
111+
Yield::Enumerator(e) => e.into_value_with(&ruby),
112+
})
113+
.map_err(|err| err.into_error(&ruby))
108114
}
109115
}
110116

111-
impl<I, T> ReturnValue for Result<Yield<I>, Error>
117+
impl<I, T> ReturnValue for Yield<I>
112118
where
113119
I: Iterator<Item = T>,
114120
T: IntoValue,
115121
{
116122
fn into_return_value(self) -> Result<Value, Error> {
117-
self?.into_return_value()
123+
Ok::<Self, Error>(self).into_return_value()
118124
}
119125
}
120126

121-
impl<I, T> ReturnValue for YieldValues<I>
127+
impl<I, T, E> ReturnValue for Result<YieldValues<I>, E>
122128
where
123129
I: Iterator<Item = T>,
124130
T: ArgList,
131+
E: IntoError,
125132
{
126133
fn into_return_value(self) -> Result<Value, Error> {
127-
match self {
134+
let ruby = unsafe { Ruby::get_unchecked() };
135+
self.map(|i| match i {
128136
YieldValues::Iter(iter) => unsafe {
129137
do_yield_values_iter(iter);
130-
Ok(Ruby::get_unchecked().qnil().as_value())
138+
ruby.qnil().as_value()
131139
},
132-
YieldValues::Enumerator(e) => Ok(unsafe { e.into_value_unchecked() }),
133-
}
140+
YieldValues::Enumerator(e) => e.into_value_with(&ruby),
141+
})
142+
.map_err(|err| err.into_error(&ruby))
134143
}
135144
}
136145

137-
impl<I, T> ReturnValue for Result<YieldValues<I>, Error>
146+
impl<I, T> ReturnValue for YieldValues<I>
138147
where
139148
I: Iterator<Item = T>,
140149
T: ArgList,
141150
{
142151
fn into_return_value(self) -> Result<Value, Error> {
143-
self?.into_return_value()
152+
Ok::<Self, Error>(self).into_return_value()
144153
}
145154
}
146155

147-
impl<I> ReturnValue for YieldSplat<I>
156+
impl<I, E> ReturnValue for Result<YieldSplat<I>, E>
148157
where
149158
I: Iterator<Item = RArray>,
159+
E: IntoError,
150160
{
151161
fn into_return_value(self) -> Result<Value, Error> {
152-
match self {
162+
let ruby = unsafe { Ruby::get_unchecked() };
163+
self.map(|i| match i {
153164
YieldSplat::Iter(iter) => unsafe {
154165
do_yield_splat_iter(iter);
155-
Ok(Ruby::get_unchecked().qnil().as_value())
166+
ruby.qnil().as_value()
156167
},
157-
YieldSplat::Enumerator(e) => Ok(unsafe { e.into_value_unchecked() }),
158-
}
168+
YieldSplat::Enumerator(e) => e.into_value_with(&ruby),
169+
})
170+
.map_err(|err| err.into_error(&ruby))
159171
}
160172
}
161173

162-
impl<I> ReturnValue for Result<YieldSplat<I>, Error>
174+
impl<I> ReturnValue for YieldSplat<I>
163175
where
164176
I: Iterator<Item = RArray>,
165177
{
166178
fn into_return_value(self) -> Result<Value, Error> {
167-
self?.into_return_value()
179+
Ok::<Self, Error>(self).into_return_value()
168180
}
169181
}
170182

@@ -178,9 +190,12 @@ mod private {
178190
}
179191
}
180192

181-
impl InitReturn for Result<(), Error> {
193+
impl<E> InitReturn for Result<(), E>
194+
where
195+
E: IntoError,
196+
{
182197
fn into_init_return(self) -> Result<(), Error> {
183-
self
198+
self.map_err(|err| err.into_error(&unsafe { Ruby::get_unchecked() }))
184199
}
185200
}
186201

tests/return_custom_error.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use magnus::{error::IntoError, function, rb_assert, Error, Ruby};
2+
3+
struct CustomError(&'static str);
4+
5+
impl IntoError for CustomError {
6+
fn into_error(self, ruby: &Ruby) -> Error {
7+
Error::new(
8+
ruby.exception_runtime_error(),
9+
format!("Custom error: {}", self.0),
10+
)
11+
}
12+
}
13+
14+
fn example() -> Result<(), CustomError> {
15+
Err(CustomError("test"))
16+
}
17+
18+
#[test]
19+
fn it_can_bind_function_returning_custom_error() {
20+
let ruby = unsafe { magnus::embed::init() };
21+
22+
ruby.define_global_function("example", function!(example, 0));
23+
24+
rb_assert!(
25+
ruby,
26+
r#"(example rescue $!).message == "Custom error: test""#
27+
);
28+
}

0 commit comments

Comments
 (0)