Skip to content
48 changes: 48 additions & 0 deletions core/src/main/java/org/jruby/embed/EmbedRubyObjectAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
*/
package org.jruby.embed;

import java.util.Map;

import org.jruby.RubyObjectAdapter;
import org.jruby.runtime.Block;

Expand Down Expand Up @@ -184,4 +186,50 @@ public interface EmbedRubyObjectAdapter extends RubyObjectAdapter {
* @return an instance of requested Java type
*/
<T> T runRubyMethod(Class<T> returnType, Object receiver, String methodName, Block block, Object... args);

/**
* Executes a method defined in Ruby script, passing keyword arguments.
*
* @param receiver is an instance that will receive this method call
* @param methodName is a method name to be called
* @param kwargs is a map of keyword argument names to values
* @return an instance automatically converted from Ruby to Java
*/
Object callMethod(Object receiver, String methodName, Map<String, Object> kwargs);

/**
* Executes a method defined in Ruby script, passing positional and keyword arguments.
*
* @param receiver is an instance that will receive this method call
* @param methodName is a method name to be called
* @param args is an array of positional method arguments
* @param kwargs is a map of keyword argument names to values
* @return an instance automatically converted from Ruby to Java
*/
Object callMethod(Object receiver, String methodName, Object[] args, Map<String, Object> kwargs);

/**
* Executes a method defined in Ruby script, passing positional and keyword arguments.
*
* @param receiver is an instance that will receive this method call
* @param methodName is a method name to be called
* @param args is an array of positional method arguments
* @param kwargs is a map of keyword argument names to values
* @param returnType is the type we want it to convert to
* @return an instance of requested Java type
*/
<T> T callMethod(Object receiver, String methodName, Object[] args, Map<String, Object> kwargs, Class<T> returnType);

/**
* Executes a method defined in Ruby script, passing positional args, keyword args, and a block.
*
* @param receiver is an instance that will receive this method call
* @param methodName is a method name to be called
* @param args is an array of positional method arguments
* @param kwargs is a map of keyword argument names to values
* @param block is a block to be executed in this method
* @param returnType is the type we want it to convert to
* @return an instance of requested Java type
*/
<T> T callMethod(Object receiver, String methodName, Object[] args, Map<String, Object> kwargs, Block block, Class<T> returnType);
}
66 changes: 66 additions & 0 deletions core/src/main/java/org/jruby/embed/ScriptingContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,72 @@ public <T> T callMethod(Object receiver, String methodName, Object[] args, Block
return objectAdapter.callMethod(receiver, methodName, args, block, returnType, unit);
}

/**
* Executes a method defined in Ruby script, passing keyword arguments.
*
* @param receiver is an instance that will receive this method call.
* Ruby's self object will be used if no appropriate receiver
* is given.
* @param methodName is a method name to be called
* @param kwargs is a map of keyword argument names (as Strings) to values.
* Keys are converted to Ruby Symbols automatically.
* @return an instance automatically converted from Ruby to Java
*/
public Object callMethod(Object receiver, String methodName, Map<String, Object> kwargs) {
return objectAdapter.callMethod(receiver, methodName, kwargs);
}

/**
* Executes a method defined in Ruby script, passing positional and keyword arguments.
*
* @param receiver is an instance that will receive this method call.
* Ruby's self object will be used if no appropriate receiver
* is given.
* @param methodName is a method name to be called
* @param args is an array of positional method arguments
* @param kwargs is a map of keyword argument names (as Strings) to values.
* Keys are converted to Ruby Symbols automatically.
* @return an instance automatically converted from Ruby to Java
*/
public Object callMethod(Object receiver, String methodName, Object[] args, Map<String, Object> kwargs) {
return objectAdapter.callMethod(receiver, methodName, args, kwargs);
}

/**
* Executes a method defined in Ruby script, passing positional and keyword arguments.
*
* @param receiver is an instance that will receive this method call.
* Ruby's self object will be used if no appropriate receiver
* is given.
* @param methodName is a method name to be called
* @param args is an array of positional method arguments
* @param kwargs is a map of keyword argument names (as Strings) to values.
* Keys are converted to Ruby Symbols automatically.
* @param returnType is the type we want it to convert to
* @return an instance of requested Java type
*/
public <T> T callMethod(Object receiver, String methodName, Object[] args, Map<String, Object> kwargs, Class<T> returnType) {
return objectAdapter.callMethod(receiver, methodName, args, kwargs, returnType);
}

/**
* Executes a method defined in Ruby script, passing positional args, keyword args, and a block.
*
* @param receiver is an instance that will receive this method call.
* Ruby's self object will be used if no appropriate receiver
* is given.
* @param methodName is a method name to be called
* @param args is an array of positional method arguments
* @param kwargs is a map of keyword argument names (as Strings) to values.
* Keys are converted to Ruby Symbols automatically.
* @param block is a block to be executed in this method
* @param returnType is the type we want it to convert to
* @return an instance of requested Java type
*/
public <T> T callMethod(Object receiver, String methodName, Object[] args, Map<String, Object> kwargs, Block block, Class<T> returnType) {
return objectAdapter.callMethod(receiver, methodName, args, kwargs, block, returnType);
}

/**
*
* @param receiver is an instance that will receive this method call.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@
*/
package org.jruby.embed.internal;

import java.util.Map;

import org.jruby.Ruby;
import org.jruby.RubyHash;
import org.jruby.RubyInteger;
import org.jruby.RubyModule;
import org.jruby.RubyObject;
import org.jruby.RubyObjectAdapter;
import org.jruby.RubyString;
import org.jruby.RubySymbol;
import org.jruby.embed.EmbedEvalUnit;
import org.jruby.embed.EmbedRubyObjectAdapter;
import org.jruby.embed.InvokeFailedException;
Expand Down Expand Up @@ -281,4 +285,78 @@ private static IRubyObject[] convertArgs(final Ruby runtime, final Object[] args
return rubyArgs;
}

// --- Keyword argument support ---

public Object callMethod(Object receiver, String methodName, Map<String, Object> kwargs) {
Comment thread
drzaiusx11 marked this conversation as resolved.
Outdated
return doInvokeMethodKwargs(Object.class, getReceiverObject(receiver), methodName, Block.NULL_BLOCK, null, null, kwargs);
}

public Object callMethod(Object receiver, String methodName, Object[] args, Map<String, Object> kwargs) {
return doInvokeMethodKwargs(Object.class, getReceiverObject(receiver), methodName, Block.NULL_BLOCK, null, args, kwargs);
}

public <T> T callMethod(Object receiver, String methodName, Object[] args, Map<String, Object> kwargs, Class<T> returnType) {
return doInvokeMethodKwargs(returnType, getReceiverObject(receiver), methodName, Block.NULL_BLOCK, null, args, kwargs);
}

public <T> T callMethod(Object receiver, String methodName, Object[] args, Map<String, Object> kwargs, Block block, Class<T> returnType) {
return doInvokeMethodKwargs(returnType, getReceiverObject(receiver), methodName, block, null, args, kwargs);
}

private <T> T doInvokeMethodKwargs(Class<T> returnType, IRubyObject rubyReceiver, String methodName,
Block block, EmbedEvalUnit unit, Object[] args, Map<String, Object> kwargs) {
final Ruby runtime = container.getProvider().getRuntime();
final ThreadContext context = runtime.getCurrentContext();
final boolean sharing_variables = isSharingVariables(container);

if (sharing_variables) {
beforeSharingVariablesCall(context, unit);
}
try {
IRubyObject[] rubyArgs = (args != null && args.length > 0) ? convertArgs(runtime, args) : IRubyObject.NULL_ARRAY;

RubyHash kwargsHash = convertKwargsToRubyHash(runtime, kwargs);

IRubyObject[] finalArgs;
if (kwargsHash != null) {
finalArgs = new IRubyObject[rubyArgs.length + 1];
System.arraycopy(rubyArgs, 0, finalArgs, 0, rubyArgs.length);
finalArgs[rubyArgs.length] = kwargsHash;
context.callInfo = ThreadContext.CALL_KEYWORD;
Comment thread
drzaiusx11 marked this conversation as resolved.
} else {
finalArgs = rubyArgs;
}

IRubyObject result = Helpers.invoke(context, rubyReceiver, methodName, finalArgs, block);
if (sharing_variables) {
container.getVarMap().retrieve(rubyReceiver);
}
if (returnType != null) {
Object ret = JavaEmbedUtils.rubyToJava(runtime, result, returnType);
return returnType.cast(ret);
}
return null;
} catch (Exception e) {
if (e instanceof InvokeFailedException) throw e;
if (wrapExceptions) throw new InvokeFailedException(e);
Helpers.throwException(e); return null; // never returns
} finally {
if (sharing_variables) {
afterSharingVariablesCall(context);
}
}
}

private static RubyHash convertKwargsToRubyHash(Ruby runtime, Map<String, Object> kwargs) {
if (kwargs == null || kwargs.isEmpty()) return null;

RubyHash hash = RubyHash.newHash(runtime);
for (Map.Entry<String, Object> entry : kwargs.entrySet()) {
RubySymbol key = runtime.newSymbol(entry.getKey());
IRubyObject value = JavaUtil.convertJavaToUsableRubyObject(runtime, entry.getValue());
hash.fastASet(key, value);
}
return hash;
}

}
94 changes: 94 additions & 0 deletions core/src/test/java/org/jruby/embed/ScriptingContainerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2664,4 +2664,98 @@ public void testJavadocExample4() {
// }
// }

@Test
public void testCallMethod_kwargs() {
ScriptingContainer instance = new ScriptingContainer(LocalContextScope.THREADSAFE);
try {
String script =
"def greet(name:, greeting: 'Hello')\n" +
" \"#{greeting}, #{name}!\"\n" +
"end\n" +
"self";
Object receiver = instance.runScriptlet(script);

Map<String, Object> kwargs = new HashMap<>();
kwargs.put("name", "World");
Object result = instance.callMethod(receiver, "greet", kwargs);
assertEquals("Hello, World!", result.toString());

kwargs.put("greeting", "Hi");
result = instance.callMethod(receiver, "greet", kwargs);
assertEquals("Hi, World!", result.toString());
} finally {
instance.terminate();
}
}

@Test
public void testCallMethod_positionalAndKwargs() {
ScriptingContainer instance = new ScriptingContainer(LocalContextScope.THREADSAFE);
try {
String script =
"def combine(a, b, separator: ', ')\n" +
" \"#{a}#{separator}#{b}\"\n" +
"end\n" +
"self";
Object receiver = instance.runScriptlet(script);

Object[] args = {"foo", "bar"};
Map<String, Object> kwargs = new HashMap<>();
kwargs.put("separator", " - ");
String result = instance.callMethod(receiver, "combine", args, kwargs, String.class);
assertEquals("foo - bar", result);
} finally {
instance.terminate();
}
}

@Test
public void testCallMethod_kwargsWithReturnType() {
ScriptingContainer instance = new ScriptingContainer(LocalContextScope.THREADSAFE);
try {
String script = "def add(x:, y:)\n x + y\nend\nself";
Object receiver = instance.runScriptlet(script);

Map<String, Object> kwargs = new HashMap<>();
kwargs.put("x", 3);
kwargs.put("y", 4);
Long result = instance.callMethod(receiver, "add", null, kwargs, Long.class);
assertEquals(Long.valueOf(7), result);
} finally {
instance.terminate();
}
}

@Test
public void testCallMethod_kwargsWithNullValue() {
ScriptingContainer instance = new ScriptingContainer(LocalContextScope.THREADSAFE);
try {
String script = "def check_nil(val: 'default')\n val.nil?\nend\nself";
Object receiver = instance.runScriptlet(script);

Map<String, Object> kwargs = new HashMap<>();
kwargs.put("val", null);
Boolean result = instance.callMethod(receiver, "check_nil", null, kwargs, Boolean.class);
assertTrue(result);
} finally {
instance.terminate();
}
}

@Test
public void testCallMethod_emptyKwargs() {
ScriptingContainer instance = new ScriptingContainer(LocalContextScope.THREADSAFE);
try {
String script = "def double(a)\n a * 2\nend\nself";
Object receiver = instance.runScriptlet(script);

Object[] args = {5};
Map<String, Object> kwargs = new HashMap<>();
Long result = instance.callMethod(receiver, "double", args, kwargs, Long.class);
assertEquals(Long.valueOf(10), result);
} finally {
instance.terminate();
}
}

}
Loading