From 99bbfd375ae19d4c99810300a2ae7f734664577c Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Sun, 22 Sep 2013 21:25:49 -0700 Subject: [PATCH 1/8] Implement JRuby function wrapping support Whenever a RubyProc is passed into an Observable method, wrap it in a Java class that implements the correct RxJava interfaces. This avoids ambiguous method errors and costly proxy instantiations by JRuby's default method delegation logic. --- language-adaptors/rxjava-jruby/build.gradle | 18 ++ .../rx/lang/jruby/JRubyActionWrapper.java | 77 ++++++++ .../rx/lang/jruby/JRubyFunctionWrapper.java | 173 ++++++++++++++++++ .../main/resources/rx/lang/jruby/interop.rb | 81 ++++++++ settings.gradle | 1 + 5 files changed, 350 insertions(+) create mode 100644 language-adaptors/rxjava-jruby/build.gradle create mode 100644 language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyActionWrapper.java create mode 100644 language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyFunctionWrapper.java create mode 100644 language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb diff --git a/language-adaptors/rxjava-jruby/build.gradle b/language-adaptors/rxjava-jruby/build.gradle new file mode 100644 index 0000000000..ef2175fa98 --- /dev/null +++ b/language-adaptors/rxjava-jruby/build.gradle @@ -0,0 +1,18 @@ +apply plugin: 'osgi' + +dependencies { + compile project(':rxjava-core') + compile 'org.jruby:jruby:1.7+' + provided 'junit:junit-dep:4.10' + provided 'org.mockito:mockito-core:1.8.5' +} + +jar { + manifest { + name = 'rxjava-jruby' + instruction 'Bundle-Vendor', 'Netflix' + instruction 'Bundle-DocURL', 'https://github.com/Netflix/RxJava' + instruction 'Import-Package', '!org.junit,!junit.framework,!org.mockito.*,*' + instruction 'Fragment-Host', 'com.netflix.rxjava.core' + } +} diff --git a/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyActionWrapper.java b/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyActionWrapper.java new file mode 100644 index 0000000000..6beb4f1d59 --- /dev/null +++ b/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyActionWrapper.java @@ -0,0 +1,77 @@ +/** + * Copyright 2013 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.lang.jruby; + +import org.jruby.RubyProc; +import org.jruby.Ruby; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.javasupport.JavaUtil; + +import rx.util.functions.Action; +import rx.util.functions.Action0; +import rx.util.functions.Action1; +import rx.util.functions.Action2; +import rx.util.functions.Action3; + +/** + * Concrete wrapper that accepts a {@link RubyProc} and produces any needed Rx {@link Action}. + * + * @param + * @param + * @param + * @param + */ +public class JRubyActionWrapper implements Action, Action0, Action1, Action2, Action3 { + + private final RubyProc proc; + private final ThreadContext context; + private final Ruby runtime; + + public JRubyActionWrapper(ThreadContext context, RubyProc proc) { + this.proc = proc; + this.context = context; + this.runtime = context.getRuntime(); + } + + @Override + public void call() { + IRubyObject[] array = new IRubyObject[0]; + proc.call(context, array); + } + + @Override + public void call(T1 t1) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1)}; + proc.call(context, array); + } + + @Override + public void call(T1 t1, T2 t2) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), + JavaUtil.convertJavaToRuby(runtime, t2)}; + proc.call(context, array); + } + + @Override + public void call(T1 t1, T2 t2, T3 t3) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), + JavaUtil.convertJavaToRuby(runtime, t2), + JavaUtil.convertJavaToRuby(runtime, t3)}; + proc.call(context, array); + } + +} diff --git a/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyFunctionWrapper.java b/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyFunctionWrapper.java new file mode 100644 index 0000000000..f049827d8e --- /dev/null +++ b/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyFunctionWrapper.java @@ -0,0 +1,173 @@ +/** + * Copyright 2013 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.lang.jruby; + +import org.jruby.RubyProc; +import org.jruby.Ruby; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.javasupport.JavaUtil; + +import rx.util.functions.Func0; +import rx.util.functions.Func1; +import rx.util.functions.Func2; +import rx.util.functions.Func3; +import rx.util.functions.Func4; +import rx.util.functions.Func5; +import rx.util.functions.Func6; +import rx.util.functions.Func7; +import rx.util.functions.Func8; +import rx.util.functions.Func9; +import rx.util.functions.FuncN; +import rx.util.functions.Function; + +/** + * Concrete wrapper that accepts a {@link RubyProc} and produces any needed Rx {@link Function}. + * + * @param + * @param + * @param + * @param + * @param + */ +public class JRubyFunctionWrapper implements + Func0, + Func1, + Func2, + Func3, + Func4, + Func5, + Func6, + Func7, + Func8, + Func9, + FuncN { + + private final RubyProc proc; + private final ThreadContext context; + private final Ruby runtime; + + public JRubyFunctionWrapper(ThreadContext context, RubyProc proc) { + this.proc = proc; + this.context = context; + this.runtime = context.getRuntime(); + } + + @Override + public R call() { + IRubyObject[] array = new IRubyObject[0]; + return (R) proc.call(context, array); + } + + @Override + public R call(T1 t1) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1)}; + return (R) proc.call(context, array); + } + + @Override + public R call(T1 t1, T2 t2) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), + JavaUtil.convertJavaToRuby(runtime, t2)}; + return (R) proc.call(context, array); + } + + @Override + public R call(T1 t1, T2 t2, T3 t3) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), + JavaUtil.convertJavaToRuby(runtime, t2), + JavaUtil.convertJavaToRuby(runtime, t3)}; + return (R) proc.call(context, array); + } + + @Override + public R call(T1 t1, T2 t2, T3 t3, T4 t4) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), + JavaUtil.convertJavaToRuby(runtime, t2), + JavaUtil.convertJavaToRuby(runtime, t3), + JavaUtil.convertJavaToRuby(runtime, t4)}; + return (R) proc.call(context, array); + } + + @Override + public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), + JavaUtil.convertJavaToRuby(runtime, t2), + JavaUtil.convertJavaToRuby(runtime, t3), + JavaUtil.convertJavaToRuby(runtime, t4), + JavaUtil.convertJavaToRuby(runtime, t5)}; + return (R) proc.call(context, array); + } + + @Override + public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), + JavaUtil.convertJavaToRuby(runtime, t2), + JavaUtil.convertJavaToRuby(runtime, t3), + JavaUtil.convertJavaToRuby(runtime, t4), + JavaUtil.convertJavaToRuby(runtime, t5), + JavaUtil.convertJavaToRuby(runtime, t6)}; + return (R) proc.call(context, array); + } + + @Override + public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), + JavaUtil.convertJavaToRuby(runtime, t2), + JavaUtil.convertJavaToRuby(runtime, t3), + JavaUtil.convertJavaToRuby(runtime, t4), + JavaUtil.convertJavaToRuby(runtime, t5), + JavaUtil.convertJavaToRuby(runtime, t6), + JavaUtil.convertJavaToRuby(runtime, t7)}; + return (R) proc.call(context, array); + } + + @Override + public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), + JavaUtil.convertJavaToRuby(runtime, t2), + JavaUtil.convertJavaToRuby(runtime, t3), + JavaUtil.convertJavaToRuby(runtime, t4), + JavaUtil.convertJavaToRuby(runtime, t5), + JavaUtil.convertJavaToRuby(runtime, t6), + JavaUtil.convertJavaToRuby(runtime, t7), + JavaUtil.convertJavaToRuby(runtime, t8)}; + return (R) proc.call(context, array); + } + + @Override + public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8, T9 t9) { + IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), + JavaUtil.convertJavaToRuby(runtime, t2), + JavaUtil.convertJavaToRuby(runtime, t3), + JavaUtil.convertJavaToRuby(runtime, t4), + JavaUtil.convertJavaToRuby(runtime, t5), + JavaUtil.convertJavaToRuby(runtime, t6), + JavaUtil.convertJavaToRuby(runtime, t7), + JavaUtil.convertJavaToRuby(runtime, t8), + JavaUtil.convertJavaToRuby(runtime, t9)}; + return (R) proc.call(context, array); + } + + @Override + public R call(Object... args) { + IRubyObject[] array = new IRubyObject[args.length]; + for (int i = 0; i < args.length; i++) { + array[i] = JavaUtil.convertJavaToRuby(runtime, args[i]); + } + return (R) proc.call(context, array); + } +} diff --git a/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb b/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb new file mode 100644 index 0000000000..478be677b3 --- /dev/null +++ b/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb @@ -0,0 +1,81 @@ +module Rx + module Lang + module Jruby + class Interop + WRAPPERS = { + Java::RxUtilFunctions::Action => Java::RxLangJruby::JRubyActionWrapper + } + + WRAPPERS.default = Java::RxLangJruby::JRubyFunctionWrapper + + KLASSES = [Java::Rx::Observable, Java::RxObservables::BlockingObservable] + FUNCTION = Java::RxUtilFunctions::Function.java_class + RUNTIME = JRuby.runtime + + def self.instance + @instance ||= new + end + + def initialize + KLASSES.each do |klass| + function_methods = (klass.java_class.declared_instance_methods + klass.java_class.declared_class_methods).select do |method| + method.public? && method.parameter_types.any? {|type| FUNCTION.assignable_from?(type)} + end + + parameter_types = {} + function_methods.each do |method| + parameter_types[[method.name, method.static?]] ||= [] + + method.parameter_types.each_with_index do |type, idx| + next unless FUNCTION.assignable_from?(type) + + constructor = WRAPPERS.find do |java_class, wrapper| + type.ruby_class.ancestors.include?(java_class) + end + + constructor = (constructor && constructor.last) || WRAPPERS.default + + parameter_types[[method.name, method.static?]][idx] ||= [] + parameter_types[[method.name, method.static?]][idx] << constructor + end + end + + parameter_types.each_pair do |_, types| + types.map! do |type| + next type.first if type && type.uniq.length == 1 + nil + end + end + + parameter_types.each_pair do |(method_name, static), types| + next if types.all?(&:nil?) + + klass_to_open = static ? klass.singleton_class : klass + + klass_to_open.send(:alias_method, "#{method_name}_without_wrapping", method_name) + klass_to_open.send(:define_method, method_name) do |*args, &block| + context = RUNTIME.get_current_context + + args = args.each_with_index.map do |arg, idx| + if arg.is_a?(Proc) && types[idx] + types[idx].new(context, arg) + else + arg + end + end + + if block && types[args.length] + block = types[args.length].new(context, block) + end + + send("#{method_name}_without_wrapping", *(args + [block].compact)) + end + end + end + end + end + end + end +end + +Rx::Lang::Jruby::Interop.instance diff --git a/settings.gradle b/settings.gradle index 8750fab727..1412d8b263 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,7 @@ rootProject.name='rxjava' include 'rxjava-core', \ 'language-adaptors:rxjava-groovy', \ 'language-adaptors:rxjava-clojure', \ +'language-adaptors:rxjava-jruby', \ 'language-adaptors:rxjava-scala', \ 'language-adaptors:rxjava-scala-java', \ 'rxjava-contrib:rxjava-swing', \ From e7eafe29507beb843bd6f8dfbbae1db3ce6de474 Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Tue, 1 Oct 2013 19:20:53 -0700 Subject: [PATCH 2/8] JRuby wrapper specs --- language-adaptors/rxjava-jruby/build.gradle | 29 +++++++++++ .../main/resources/rx/lang/jruby/interop.rb | 44 +++++++++++----- .../spec/ruby/rx/lang/jruby/interop_spec.rb | 43 +++++++++++++++ .../lang/jruby/jruby_action_wrapper_spec.rb | 32 ++++++++++++ .../lang/jruby/jruby_function_wrapper_spec.rb | 52 +++++++++++++++++++ .../spec/ruby/rx/lang/jruby/spec_helper.rb | 1 + 6 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb create mode 100644 language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_action_wrapper_spec.rb create mode 100644 language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_function_wrapper_spec.rb create mode 100644 language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/spec_helper.rb diff --git a/language-adaptors/rxjava-jruby/build.gradle b/language-adaptors/rxjava-jruby/build.gradle index ef2175fa98..e1774be16e 100644 --- a/language-adaptors/rxjava-jruby/build.gradle +++ b/language-adaptors/rxjava-jruby/build.gradle @@ -1,12 +1,41 @@ apply plugin: 'osgi' +repositories { + maven { + url 'http://deux.gemjars.org' + } + mavenCentral() +} + +configurations { + rspec +} + +sourceSets { + test { + resources { + srcDir 'src/spec/ruby' + } + } +} + dependencies { compile project(':rxjava-core') compile 'org.jruby:jruby:1.7+' provided 'junit:junit-dep:4.10' provided 'org.mockito:mockito-core:1.8.5' + rspec 'org.jruby:jruby-complete:1.7.4' + rspec 'org.rubygems:rspec:2.10.0' } +task(rspec, type: JavaExec) { + main 'org.jruby.Main' + classpath configurations.rspec + runtimeClasspath + args 'classpath:bin/rspec', 'src/spec/ruby' +} + +tasks.build.dependsOn << 'rspec' + jar { manifest { name = 'rxjava-jruby' diff --git a/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb b/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb index 478be677b3..a782c3e22c 100644 --- a/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb +++ b/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb @@ -3,7 +3,7 @@ module Lang module Jruby class Interop WRAPPERS = { - Java::RxUtilFunctions::Action => Java::RxLangJruby::JRubyActionWrapper + Java::RxUtilFunctions::Action => Java::RxLangJruby::JRubyActionWrapper } WRAPPERS.default = Java::RxLangJruby::JRubyFunctionWrapper @@ -52,27 +52,43 @@ def initialize klass_to_open = static ? klass.singleton_class : klass - klass_to_open.send(:alias_method, "#{method_name}_without_wrapping", method_name) - klass_to_open.send(:define_method, method_name) do |*args, &block| - context = RUNTIME.get_current_context + [method_name, underscore(method_name)].each do |name| + klass_to_open.send(:alias_method, "#{name}_without_wrapping", name) + klass_to_open.send(:define_method, name) do |*args, &block| + context = RUNTIME.get_current_context + + args = args.each_with_index.map do |arg, idx| + if arg.is_a?(Proc) && types[idx] + types[idx].new(context, arg) + else + arg + end + end - args = args.each_with_index.map do |arg, idx| - if arg.is_a?(Proc) && types[idx] - types[idx].new(context, arg) - else - arg + if block && types[args.length] + block = types[args.length].new(context, block) end - end - if block && types[args.length] - block = types[args.length].new(context, block) + send("#{name}_without_wrapping", *(args + [block].compact)) end - - send("#{method_name}_without_wrapping", *(args + [block].compact)) end end end end + + private + + # File activesupport/lib/active_support/inflector/methods.rb, line 89 + def underscore(camel_cased_word) + word = camel_cased_word.to_s.dup + word.gsub!('::', '/') + word.gsub!(/(?:([A-Za-z\d])|^)((?=a)b)(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" } + word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') + word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') + word.tr!("-", "_") + word.downcase! + word + end end end end diff --git a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb new file mode 100644 index 0000000000..cf41bfc98a --- /dev/null +++ b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb @@ -0,0 +1,43 @@ +require_relative "spec_helper" + +describe Rx::Lang::Jruby::Interop do + subject { described_class.instance } + + let(:observable) { Java::Rx::Observable.from([1, 2, 3]) } + + context "with a normal, non-function method signature" do + it "calls straight through to the original Java method" do + observable.should_not_receive(:toBlockingObservable_without_wrapping) + observable.toBlockingObservable.should be_a(Java::RxObservables::BlockingObservable) + end + end + + context "with a method with a function method signature" do + it "wraps function arguments if they're in the right position" do + observable.should_receive(:subscribe_without_wrapping).with(kind_of(Java::RxLangJruby::JRubyActionWrapper)) + observable.subscribe(lambda {}) + end + + it "doesn't wrap function arguments if they're in the wrong position" do + proc = lambda {} + observable.should_receive(:subscribe_without_wrapping).with(1, 1, 1, proc) + observable.subscribe(1, 1, 1, proc) + end + + it "doesn't wrap non-function arguments" do + observable.should_receive(:subscribe_without_wrapping).with(1) + observable.subscribe(1) + end + + it "works with underscoreized method names" do + observable.should_receive(:finally_do_without_wrapping).with(kind_of(Java::RxLangJruby::JRubyActionWrapper)) + observable.finally_do(lambda {}) + end + + it "passes a block through as the last argument" do + proc = lambda {} + observable.should_receive(:subscribe_without_wrapping).with(1, 1, 1, 1, proc) + observable.subscribe(1, 1, 1, 1, &proc) # intentionally bogus call so it doesn't wrap the proc + end + end +end diff --git a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_action_wrapper_spec.rb b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_action_wrapper_spec.rb new file mode 100644 index 0000000000..708ee965c1 --- /dev/null +++ b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_action_wrapper_spec.rb @@ -0,0 +1,32 @@ +require_relative "spec_helper" + +describe Java::RxLangJruby::JRubyActionWrapper do + let(:spy) { double(:spy, :call => nil) } + subject { described_class.new(JRuby.runtime.get_current_context, lambda {|*args| spy.call(args)}) } + + let(:interfaces) do + [Java::RxUtilFunctions::Action, + Java::RxUtilFunctions::Action0, + Java::RxUtilFunctions::Action1, + Java::RxUtilFunctions::Action2, + Java::RxUtilFunctions::Action3] + end + + it "implements the interfaces" do + interfaces.each do |interface| + subject.is_a?(interface) + end + end + + it "successfully uses the interfaces" do + spy.should_receive(:call).with([]) + spy.should_receive(:call).with([1]) + spy.should_receive(:call).with([1, 2]) + spy.should_receive(:call).with([1, 2, 3]) + + subject.call + subject.call(1) + subject.call(1, 2) + subject.call(1, 2, 3) + end +end diff --git a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_function_wrapper_spec.rb b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_function_wrapper_spec.rb new file mode 100644 index 0000000000..34597c171a --- /dev/null +++ b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_function_wrapper_spec.rb @@ -0,0 +1,52 @@ +require_relative "spec_helper" + +describe Java::RxLangJruby::JRubyFunctionWrapper do + let(:spy) { double(:spy, :call => nil) } + subject { described_class.new(JRuby.runtime.get_current_context, lambda {|*args| spy.call(args); args}) } + + let(:interfaces) do + [Java::RxUtilFunctions::Func0, + Java::RxUtilFunctions::Func1, + Java::RxUtilFunctions::Func2, + Java::RxUtilFunctions::Func3, + Java::RxUtilFunctions::Func4, + Java::RxUtilFunctions::Func5, + Java::RxUtilFunctions::Func6, + Java::RxUtilFunctions::Func7, + Java::RxUtilFunctions::Func8, + Java::RxUtilFunctions::Func9, + Java::RxUtilFunctions::FuncN] + end + + it "implements the interfaces" do + interfaces.each do |interface| + subject.is_a?(interface) + end + end + + it "successfully uses the interfaces" do + spy.should_receive(:call).with([]) + spy.should_receive(:call).with([1]) + spy.should_receive(:call).with([1, 2]) + spy.should_receive(:call).with([1, 2, 3]) + spy.should_receive(:call).with([1, 2, 3, 4]) + spy.should_receive(:call).with([1, 2, 3, 4, 5]) + spy.should_receive(:call).with([1, 2, 3, 4, 5, 6]) + spy.should_receive(:call).with([1, 2, 3, 4, 5, 6, 7]) + spy.should_receive(:call).with([1, 2, 3, 4, 5, 6, 7, 8]) + spy.should_receive(:call).with([1, 2, 3, 4, 5, 6, 7, 8, 9]) + spy.should_receive(:call).with([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + + subject.call.should == [] + subject.call(1).should == [1] + subject.call(1, 2).should == [1, 2] + subject.call(1, 2, 3).should == [1, 2, 3] + subject.call(1, 2, 3, 4).should == [1, 2, 3, 4] + subject.call(1, 2, 3, 4, 5).should == [1, 2, 3, 4, 5] + subject.call(1, 2, 3, 4, 5, 6).should == [1, 2, 3, 4, 5, 6] + subject.call(1, 2, 3, 4, 5, 6, 7).should == [1, 2, 3, 4, 5, 6, 7] + subject.call(1, 2, 3, 4, 5, 6, 7, 8).should == [1, 2, 3, 4, 5, 6, 7, 8] + subject.call(1, 2, 3, 4, 5, 6, 7, 8, 9).should == [1, 2, 3, 4, 5, 6, 7, 8, 9] + subject.call(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).should == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + end +end diff --git a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/spec_helper.rb b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/spec_helper.rb new file mode 100644 index 0000000000..c8451bc051 --- /dev/null +++ b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/spec_helper.rb @@ -0,0 +1 @@ +require "rx/lang/jruby/interop" From 2f44a8d8c17f369162c4b54b87205e86c25640ef Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Thu, 26 Sep 2013 22:53:07 -0700 Subject: [PATCH 3/8] JRuby performance test --- .../spec/ruby/rx/lang/jruby/performance.rb | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/performance.rb diff --git a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/performance.rb b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/performance.rb new file mode 100644 index 0000000000..200273f69e --- /dev/null +++ b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/performance.rb @@ -0,0 +1,38 @@ +# Current execution times: +# Without rxjava-jruby: 3.31s +# With rxjava-jruby: 2.18s + +require 'optparse' + +options = {} +OptionParser.new do |opts| + opts.banner = "Usage: jruby --profile.api performance.rb [options]" + + opts.on("-c", "--core CORE-PATH", "Path to the rxjava-core.jar") {|core| options[:core] = core} + opts.on("-j", "--jruby [JRUBY-PATH]", "Path to the rxjava-jruby.jar (optional)") {|jruby| options[:jruby] = jruby} +end.parse! + +require options[:core] + +if options[:jruby] + require options[:jruby] + require 'rx/lang/jruby/interop' +end + +require 'jruby/profiler' + +profile_data = JRuby::Profiler.profile do + 10000.times do + o = Java::Rx::Observable.create do |observer| + observer.onNext("one") + observer.onNext("two") + observer.onNext("three") + observer.onCompleted + Java::RxSubscriptions::Subscription.empty + end + o.map {|n| n * 2}.subscribe {|n| n} + end +end + +profile_printer = JRuby::Profiler::FlatProfilePrinter.new(profile_data) +profile_printer.printProfile(STDOUT) From fd910988f182d0673b42f891419b6e8d44a340da Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Sun, 6 Oct 2013 12:42:57 -0700 Subject: [PATCH 4/8] Handle OnSubscribeFuncs correctly in JRuby interop logic OnSubscribeFunc#onSubscribe expects to return a Subscription. I am unable to successfully cast the return value of a RubyProc, even if that value _is_ an object that implements the Subscription interface, into a Subscription in Java-land (Java reports that ConcreteJavaProxy cannot be cast). Instead I allow JRuby to handle OnSubscribeFunc arguments through its default proxy logic, which works correctly. --- .../src/main/resources/rx/lang/jruby/interop.rb | 8 ++++++-- .../src/spec/ruby/rx/lang/jruby/interop_spec.rb | 12 +++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb b/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb index a782c3e22c..899ec12002 100644 --- a/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb +++ b/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb @@ -3,7 +3,8 @@ module Lang module Jruby class Interop WRAPPERS = { - Java::RxUtilFunctions::Action => Java::RxLangJruby::JRubyActionWrapper + Java::RxUtilFunctions::Action => Java::RxLangJruby::JRubyActionWrapper, + Java::Rx::Observable::OnSubscribeFunc => false } WRAPPERS.default = Java::RxLangJruby::JRubyFunctionWrapper @@ -33,6 +34,9 @@ def initialize type.ruby_class.ancestors.include?(java_class) end + # Skip OnSubscribeFuncs + next if constructor && constructor.last == false + constructor = (constructor && constructor.last) || WRAPPERS.default parameter_types[[method.name, method.static?]][idx] ||= [] @@ -52,7 +56,7 @@ def initialize klass_to_open = static ? klass.singleton_class : klass - [method_name, underscore(method_name)].each do |name| + [method_name, underscore(method_name)].uniq.each do |name| klass_to_open.send(:alias_method, "#{name}_without_wrapping", name) klass_to_open.send(:define_method, name) do |*args, &block| context = RUNTIME.get_current_context diff --git a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb index cf41bfc98a..45ddb20d13 100644 --- a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb +++ b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb @@ -29,8 +29,18 @@ observable.subscribe(1) end + it "doesn't wrap OnSubscribeFunc arguments" do + proc = lambda {|observer| observer.onNext("hi")} + Java::Rx::Observable.should_not_receive(:create_without_wrapping) + Java::Rx::Observable.create(proc).should be_a(Java::Rx::Observable) + end + it "works with underscoreized method names" do - observable.should_receive(:finally_do_without_wrapping).with(kind_of(Java::RxLangJruby::JRubyActionWrapper)) + observable. + should_receive(:finally_do_without_wrapping). + with(kind_of(Java::RxLangJruby::JRubyActionWrapper)). + and_call_original + observable.finally_do(lambda {}) end From e1b81b43eb9bde1e9314ad5af19965f3f42c0b9b Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Sun, 6 Oct 2013 16:54:59 -0700 Subject: [PATCH 5/8] JRuby README --- language-adaptors/rxjava-jruby/README.md | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 language-adaptors/rxjava-jruby/README.md diff --git a/language-adaptors/rxjava-jruby/README.md b/language-adaptors/rxjava-jruby/README.md new file mode 100644 index 0000000000..3232e073f9 --- /dev/null +++ b/language-adaptors/rxjava-jruby/README.md @@ -0,0 +1,37 @@ +# JRuby Adaptor for RxJava + +This adaptor improves the success and performance of RxJava when Ruby `Proc` is passed to an RxJava method. + +This enables correct and efficient execution of code such as: + +```ruby + Observable.from("one", "two", "three"). + take(2). + subscribe {|val| puts val} +``` + +# Binaries + +Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-jruby%22). + +Example for Maven: + +```xml + + com.netflix.rxjava + rxjava-jruby + x.y.z + +``` + +and for Ivy: + +```xml + +``` + +and for Gradle: + +```groovy +compile 'com.netflix.rxjava:rxjava-jruby:x.y.z' +``` From 16a773544793491d525a190c1b3baa1c10fec613 Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Sun, 6 Oct 2013 17:49:37 -0700 Subject: [PATCH 6/8] JRuby license headers --- .../src/main/resources/rx/lang/jruby/interop.rb | 14 ++++++++++++++ .../src/spec/ruby/rx/lang/jruby/interop_spec.rb | 15 +++++++++++++++ .../rx/lang/jruby/jruby_action_wrapper_spec.rb | 14 ++++++++++++++ .../rx/lang/jruby/jruby_function_wrapper_spec.rb | 14 ++++++++++++++ .../src/spec/ruby/rx/lang/jruby/performance.rb | 14 ++++++++++++++ .../src/spec/ruby/rx/lang/jruby/spec_helper.rb | 14 ++++++++++++++ 6 files changed, 85 insertions(+) diff --git a/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb b/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb index 899ec12002..a080639728 100644 --- a/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb +++ b/language-adaptors/rxjava-jruby/src/main/resources/rx/lang/jruby/interop.rb @@ -1,3 +1,17 @@ +# Copyright 2013 Mike Ragalie +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + module Rx module Lang module Jruby diff --git a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb index 45ddb20d13..222caf34ed 100644 --- a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb +++ b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/interop_spec.rb @@ -1,3 +1,17 @@ +# Copyright 2013 Mike Ragalie +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + require_relative "spec_helper" describe Rx::Lang::Jruby::Interop do @@ -31,6 +45,7 @@ it "doesn't wrap OnSubscribeFunc arguments" do proc = lambda {|observer| observer.onNext("hi")} + Java::Rx::Observable.__persistent__ = true Java::Rx::Observable.should_not_receive(:create_without_wrapping) Java::Rx::Observable.create(proc).should be_a(Java::Rx::Observable) end diff --git a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_action_wrapper_spec.rb b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_action_wrapper_spec.rb index 708ee965c1..dae1973625 100644 --- a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_action_wrapper_spec.rb +++ b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_action_wrapper_spec.rb @@ -1,3 +1,17 @@ +# Copyright 2013 Mike Ragalie +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + require_relative "spec_helper" describe Java::RxLangJruby::JRubyActionWrapper do diff --git a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_function_wrapper_spec.rb b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_function_wrapper_spec.rb index 34597c171a..4c25f6ec1c 100644 --- a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_function_wrapper_spec.rb +++ b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/jruby_function_wrapper_spec.rb @@ -1,3 +1,17 @@ +# Copyright 2013 Mike Ragalie +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + require_relative "spec_helper" describe Java::RxLangJruby::JRubyFunctionWrapper do diff --git a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/performance.rb b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/performance.rb index 200273f69e..fc9cde1b0b 100644 --- a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/performance.rb +++ b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/performance.rb @@ -1,3 +1,17 @@ +# Copyright 2013 Mike Ragalie +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # Current execution times: # Without rxjava-jruby: 3.31s # With rxjava-jruby: 2.18s diff --git a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/spec_helper.rb b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/spec_helper.rb index c8451bc051..99b095960f 100644 --- a/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/spec_helper.rb +++ b/language-adaptors/rxjava-jruby/src/spec/ruby/rx/lang/jruby/spec_helper.rb @@ -1 +1,15 @@ +# Copyright 2013 Mike Ragalie +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + require "rx/lang/jruby/interop" From 13992a1b73796d05d109c59deaef77a64d0daf4b Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Sun, 6 Oct 2013 18:29:51 -0700 Subject: [PATCH 7/8] Up rspec version --- language-adaptors/rxjava-jruby/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/language-adaptors/rxjava-jruby/build.gradle b/language-adaptors/rxjava-jruby/build.gradle index e1774be16e..eaefe7a81a 100644 --- a/language-adaptors/rxjava-jruby/build.gradle +++ b/language-adaptors/rxjava-jruby/build.gradle @@ -25,7 +25,7 @@ dependencies { provided 'junit:junit-dep:4.10' provided 'org.mockito:mockito-core:1.8.5' rspec 'org.jruby:jruby-complete:1.7.4' - rspec 'org.rubygems:rspec:2.10.0' + rspec 'org.rubygems:rspec:2.14.1' } task(rspec, type: JavaExec) { From 24807e94506715701e944c0ef4d554156c93e01b Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Sun, 6 Oct 2013 18:33:36 -0700 Subject: [PATCH 8/8] JRuby README include usage instructions --- language-adaptors/rxjava-jruby/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/language-adaptors/rxjava-jruby/README.md b/language-adaptors/rxjava-jruby/README.md index 3232e073f9..da75bac1fb 100644 --- a/language-adaptors/rxjava-jruby/README.md +++ b/language-adaptors/rxjava-jruby/README.md @@ -10,6 +10,14 @@ This enables correct and efficient execution of code such as: subscribe {|val| puts val} ``` +# Usage + +Require the JAR file as usual. After requiring the JAR, you must also require the interop code: + +```ruby +require "rx/lang/jruby/interop" +``` + # Binaries Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-jruby%22).