Skip to content

Commit

Permalink
Merge pull request matsadler#80 from adampetro/funcall-kw
Browse files Browse the repository at this point in the history
Add `funcall_kw` and `new_instance_kw` methods
  • Loading branch information
matsadler authored Jun 26, 2023
2 parents 8e92f76 + 7ceba7f commit 571fa45
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 28 deletions.
26 changes: 17 additions & 9 deletions src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ use std::{

use rb_sys::{
rb_block_given_p, rb_block_proc, rb_data_typed_object_wrap, rb_obj_is_proc, rb_proc_arity,
rb_proc_call, rb_proc_lambda_p, rb_proc_new, rb_yield, rb_yield_splat, rb_yield_values2, VALUE,
rb_proc_call, rb_proc_lambda_p, rb_proc_new, rb_yield, rb_yield_splat, rb_yield_values_kw,
VALUE,
};

use crate::{
data_type_builder,
enumerator::Enumerator,
error::{ensure, protect, Error},
gc,
into_value::{ArgList, IntoValue, RArrayArgList},
into_value::{kw_splat, ArgList, IntoValue, RArrayArgList},
method::{Block, BlockReturn},
object::Object,
r_array::RArray,
Expand Down Expand Up @@ -604,12 +605,12 @@ impl Ruby {
/// # Examples
///
/// ```
/// use magnus::{function, rb_assert, Error, Ruby, Value};
/// use magnus::{function, kwargs, rb_assert, Error, Ruby, Value};
///
/// fn metasyntactic_variables(ruby: &Ruby) -> Result<(), Error> {
/// let _: Value = ruby.yield_values((0, "foo"))?;
/// let _: Value = ruby.yield_values((1, "bar"))?;
/// let _: Value = ruby.yield_values((2, "baz"))?;
/// let _: Value = ruby.yield_values((0, kwargs!("var" => "foo")))?;
/// let _: Value = ruby.yield_values((1, kwargs!("var" => "bar")))?;
/// let _: Value = ruby.yield_values((2, kwargs!("var" => "baz")))?;
/// Ok(())
/// }
///
Expand All @@ -622,7 +623,7 @@ impl Ruby {
/// let vars = ruby.ary_new();
/// rb_assert!(
/// ruby,
/// "metasyntactic_variables {|pos, var| vars << [pos, var]} == nil",
/// "metasyntactic_variables {|pos, var:| vars << [pos, var]} == nil",
/// vars
/// );
/// rb_assert!(
Expand All @@ -640,13 +641,15 @@ impl Ruby {
T: ArgList,
U: TryConvert,
{
let kw_splat = kw_splat(&vals);
let vals = vals.into_arg_list_with(self);
let slice = vals.as_ref();
unsafe {
protect(|| {
Value::new(rb_yield_values2(
Value::new(rb_yield_values_kw(
slice.len() as c_int,
slice.as_ptr() as *const VALUE,
kw_splat as c_int,
))
})
.and_then(TryConvert::try_convert)
Expand Down Expand Up @@ -986,9 +989,14 @@ where
ensure(
|| {
for val in &mut *ptr {
let kw_splat = kw_splat(&val);
let vals = val.into_arg_list_with(&handle);
let slice = vals.as_ref();
rb_yield_values2(slice.len() as c_int, slice.as_ptr() as *const VALUE);
rb_yield_values_kw(
slice.len() as c_int,
slice.as_ptr() as *const VALUE,
kw_splat as c_int,
);
}
handle.qnil()
},
Expand Down
56 changes: 53 additions & 3 deletions src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ use rb_sys::{
rb_cInteger, rb_cMatch, rb_cMethod, rb_cModule, rb_cNameErrorMesg, rb_cNilClass, rb_cNumeric,
rb_cObject, rb_cProc, rb_cRandom, rb_cRange, rb_cRational, rb_cRegexp, rb_cStat, rb_cString,
rb_cStruct, rb_cSymbol, rb_cThread, rb_cTime, rb_cTrueClass, rb_cUnboundMethod, rb_class2name,
rb_class_new, rb_class_new_instance, rb_class_superclass, rb_define_alloc_func,
rb_class_new, rb_class_new_instance_kw, rb_class_superclass, rb_define_alloc_func,
rb_get_alloc_func, rb_obj_alloc, rb_undef_alloc_func, ruby_value_type, VALUE,
};

use crate::{
error::{protect, Error},
into_value::{ArgList, IntoValue},
into_value::{kw_splat, ArgList, IntoValue},
module::Module,
object::Object,
try_convert::TryConvert,
Expand Down Expand Up @@ -150,6 +150,33 @@ pub trait Class: Module {
/// ```
///
/// ```
/// use magnus::{class, eval, kwargs, prelude::*, RClass, RHash};
/// # let _cleanup = unsafe { magnus::embed::init() };
///
/// let cls: RClass = eval!(
/// r#"
/// class Foo
/// def initialize(bar, baz:)
/// @bar = bar
/// @baz = baz
/// end
///
/// attr_reader(:bar, :baz)
/// end
///
/// Object.const_get(:Foo)
/// "#
/// )
/// .unwrap();
/// let instance = cls.new_instance((1, kwargs!("baz" => 2))).unwrap();
/// assert!(instance.is_kind_of(cls));
/// let bar: i32 = instance.funcall("bar", ()).unwrap();
/// assert_eq!(bar, 1);
/// let baz: i32 = instance.funcall("baz", ()).unwrap();
/// assert_eq!(baz, 2);
/// ```
///
/// ```
/// use magnus::{exception, prelude::*};
/// # let _cleanup = unsafe { magnus::embed::init() };
///
Expand All @@ -158,6 +185,27 @@ pub trait Class: Module {
/// .unwrap();
/// assert!(s.is_kind_of(exception::standard_error()));
/// ```
///
/// ```
/// use magnus::{eval, ExceptionClass, kwargs, prelude::*, RHash};
/// # let _cleanup = unsafe { magnus::embed::init() };
///
/// let exc: ExceptionClass = eval!(
/// r#"
/// class MyError < StandardError
/// def initialize(message:)
/// super(message)
/// end
/// end
///
/// Object.const_get(:MyError)
/// "#
/// ).unwrap();
/// let s = exc.new_instance((kwargs!("message" => "bang!"),)).unwrap();
/// assert!(s.is_kind_of(exc));
/// let message: String = s.funcall("message", ()).unwrap();
/// assert_eq!(message, "bang!");
/// ```
fn new_instance<T>(self, args: T) -> Result<Self::Instance, Error>
where
T: ArgList;
Expand Down Expand Up @@ -440,14 +488,16 @@ impl Class for RClass {
where
T: ArgList,
{
let kw_splat = kw_splat(&args);
let args = args.into_arg_list_with(&Ruby::get_with(self));
let slice = args.as_ref();
unsafe {
protect(|| {
Value::new(rb_class_new_instance(
Value::new(rb_class_new_instance_kw(
slice.len() as c_int,
slice.as_ptr() as *const VALUE,
self.as_rb_value(),
kw_splat as c_int,
))
})
}
Expand Down
107 changes: 107 additions & 0 deletions src/into_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use seq_macro::seq;

use crate::{
r_array::RArray,
r_hash::RHash,
value::{ReprValue, Value},
Ruby,
};
Expand Down Expand Up @@ -77,6 +78,73 @@ pub trait ArgList {

/// Convert `self` into a type that can be used as a Ruby argument list.
fn into_arg_list_with(self, handle: &Ruby) -> Self::Output;

/// Whether the argument list contains keyword arguments. If true, the
/// last element of the `&[Self::Value]` produced by
/// `Self::into_arg_list_with` and [`AsRef`]
fn contains_kw_args(&self) -> bool;
}

pub(crate) fn kw_splat(args: &impl ArgList) -> u32 {
if args.contains_kw_args() {
rb_sys::RB_PASS_KEYWORDS
} else {
rb_sys::RB_NO_KEYWORDS
}
}

/// Wrapper for [`RHash`] intended for use in the tuple implementations of
/// [`ArgList`] to indicate that the last argument in the tuple is to be
/// passed as keyword arguments.
#[repr(transparent)]
pub struct KwArgs(pub RHash);

/// Create a [`KwArgs`] from Rust key-value mappings. Keys must be string
/// literals, while values can be anything that implements [`IntoValue`].
///
/// # Panics
///
/// Panics if called from a non-Ruby thread.
///
/// # Examples
///
/// ```
/// use magnus::{eval, kwargs, KwArgs, prelude::*, RObject};
/// # let _cleanup = unsafe { magnus::embed::init() };
///
/// let kwargs = kwargs!("a" => 1, "b" => 2);
/// let object: RObject = eval!(
/// r#"
/// class Adder
/// def add(a:, b:)
/// a + b
/// end
/// end
///
/// Adder.new
/// "#
/// )
/// .unwrap();
///
/// let result: i32 = object.funcall("add", (kwargs,)).unwrap();
/// assert_eq!(3, result);
/// ```
#[macro_export]
macro_rules! kwargs {
($ruby:expr, $($k:literal => $v:expr),+) => {{
use $crate::IntoValue;
let h = $ruby.hash_new();
$(
h.aset(
$ruby.to_symbol($k),
$v.into_value_with($ruby),
).unwrap();
)+
$crate::KwArgs(h)
}};
($($k:literal => $v:expr),+) => {{
$crate::kwargs!(&$crate::Ruby::get().unwrap(), $($k => $v),+)
}};
}

/// # Safety
Expand All @@ -94,6 +162,10 @@ where
fn into_arg_list_with(self, _: &Ruby) -> Self::Output {
self
}

fn contains_kw_args(&self) -> bool {
false
}
}

macro_rules! impl_arg_list {
Expand All @@ -110,6 +182,10 @@ macro_rules! impl_arg_list {
fn into_arg_list_with(self, handle: &Ruby) -> Self::Output {
[#(handle.into_value(self.N),)*]
}

fn contains_kw_args(&self) -> bool {
false
}
}
});
}
Expand All @@ -119,6 +195,33 @@ seq!(N in 0..=12 {
impl_arg_list!(N);
});

macro_rules! impl_arg_list_kw {
($n:literal) => {
seq!(N in 0..$n {
impl<#(T~N,)*> ArgList for (#(T~N,)* KwArgs,)
where
#(T~N: IntoValue,)*
{
type Value = Value;
type Output = [Self::Value; { $n + 1 }];

#[allow(unused_variables)]
fn into_arg_list_with(self, handle: &Ruby) -> Self::Output {
[#(handle.into_value(self.N),)* handle.into_value(self.$n.0)]
}

fn contains_kw_args(&self) -> bool {
true
}
}
});
}
}

seq!(N in 0..=12 {
impl_arg_list_kw!(N);
});

impl<T, const N: usize> ArgList for [T; N]
where
T: ReprValue,
Expand All @@ -129,6 +232,10 @@ where
fn into_arg_list_with(self, _: &Ruby) -> Self::Output {
self
}

fn contains_kw_args(&self) -> bool {
false
}
}

/// Trait for types that can be used as an arguments list when calling Ruby
Expand Down
8 changes: 5 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1847,7 +1847,7 @@ use ::rb_sys::rb_require;
#[cfg(ruby_gte_2_7)]
use ::rb_sys::rb_require_string;
use ::rb_sys::{
rb_backref_get, rb_call_super, rb_current_receiver, rb_define_class, rb_define_global_const,
rb_backref_get, rb_call_super_kw, rb_current_receiver, rb_define_class, rb_define_global_const,
rb_define_global_function, rb_define_module, rb_define_variable, rb_errinfo,
rb_eval_string_protect, rb_set_errinfo, VALUE,
};
Expand All @@ -1863,7 +1863,7 @@ pub use crate::{
exception::{Exception, ExceptionClass},
float::Float,
integer::Integer,
into_value::{ArgList, IntoValue, IntoValueFromNative, RArrayArgList},
into_value::{ArgList, IntoValue, IntoValueFromNative, KwArgs, RArrayArgList},
module::{Attr, Module, RModule},
numeric::Numeric,
object::Object,
Expand Down Expand Up @@ -2270,12 +2270,14 @@ impl Ruby {
T: TryConvert,
{
unsafe {
let kw_splat = into_value::kw_splat(&args);
let args = args.into_arg_list_with(self);
let slice = args.as_ref();
protect(|| {
Value::new(rb_call_super(
Value::new(rb_call_super_kw(
slice.len() as c_int,
slice.as_ptr() as *const VALUE,
kw_splat as c_int,
))
})
.and_then(TryConvert::try_convert)
Expand Down
Loading

0 comments on commit 571fa45

Please sign in to comment.