From 11f4df4be9f53ea7d68efe13af2aae1a3c023b04 Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 8 Dec 2011 17:28:23 +0100 Subject: [PATCH] RubyObject/RubyFunction review + specs for better integration when some ruby needs to enter Rhino's JS world ... --- lib/rhino/ruby_function.rb | 18 ++++- lib/rhino/ruby_object.rb | 121 ++++++++++++++++++------------ lib/rhino/wormhole.rb | 4 + spec/rhino/context_spec.rb | 2 +- spec/rhino/ruby_function_spec.rb | 40 ++++++++++ spec/rhino/ruby_object_spec.rb | 123 +++++++++++++++++++++++++++++++ spec/rhino/wormhole_spec.rb | 34 ++++----- spec/spec_helper.rb | 1 + therubyrhino.gemspec | 1 + 9 files changed, 275 insertions(+), 69 deletions(-) create mode 100644 spec/rhino/ruby_function_spec.rb create mode 100644 spec/rhino/ruby_object_spec.rb diff --git a/lib/rhino/ruby_function.rb b/lib/rhino/ruby_function.rb index 23a63e7..7908ff2 100644 --- a/lib/rhino/ruby_function.rb +++ b/lib/rhino/ruby_function.rb @@ -7,8 +7,22 @@ module Rhino @callable = callable end - def call(cxt, scope, this, args) - To.javascript @callable.call(*Array(args).map {|a| To.ruby(a)}) + def unwrap + @callable end + + # override Object BaseFunction#call(Context context, Scriptable scope, + # Scriptable thisObj, Object[] args) + def call(context, scope, this, args) + rb_args = To.args_to_ruby(args.to_a) + begin + result = @callable.call(*rb_args) + rescue => e + # ... correct wrapping thus it's try { } catch (e) works in JS : + raise JS::WrappedException.new(org.jruby.exceptions.RaiseException.new(e)) + end + To.to_javascript(result, scope) + end + end end \ No newline at end of file diff --git a/lib/rhino/ruby_object.rb b/lib/rhino/ruby_object.rb index 7767ad0..42afadd 100644 --- a/lib/rhino/ruby_object.rb +++ b/lib/rhino/ruby_object.rb @@ -8,64 +8,89 @@ module Rhino @ruby = object end + # abstract Object Wrapper#unwrap(); def unwrap @ruby end - def getClassName() + # abstract String Scriptable#getClassName(); + def getClassName @ruby.class.name end - - def getPrototype() - Prototype::Generic + + def toString + "[ruby #{getClassName}]" # [object User] end - def put(key, start, value) - if @ruby.respond_to?("#{key}=") - @ruby.send("#{key}=", To.ruby(value)) - value - else - super - end - end - - def getIds() - @ruby.public_methods(false).map {|m| m.gsub(/(.)_(.)/) {java.lang.String.new("#{$1}#{$2.upcase}")}}.to_java - end - - def to_s - "[Native #{@ruby.class.name}]" - end - - alias_method :prototype, :getPrototype - - - class Prototype < JS::ScriptableObject - - def get(name, start) - robject = To.ruby(start) - if name == "toString" - return RubyFunction.new(lambda { "[Ruby #{robject.class.name}]"}) - end - rb_name = name.gsub(/([a-z])([A-Z])/) {"#{$1}_#{$2.downcase}"}.to_sym - if (robject.public_methods(false).collect(&:to_sym).include?(rb_name)) - method = robject.method(rb_name) - if method.arity == 0 - To.javascript(method.call) - else - RubyFunction.new(method) - end - else - super(name, start) + # override Object Scriptable#get(String name, Scriptable start); + # override Object Scriptable#get(int index, Scriptable start); + def get(name, start) + if name.is_a?(String) + # NOTE: preferrably when using a ruby object in JS methods should + # be used but instance variables will work as well but if there's + # a attr reader it is given a preference e.g. : + # + # class Foo + # attr_reader :bar2 + # def initialize + # @bar1 = 'bar1' + # @bar2 = 'bar2' + # end + # end + # + # fooObj.bar1; // 'bar1' + # fooObj.bar2; // function + # fooObj.bar2(); // 'bar2' + # + if @ruby.respond_to?(name) + return RubyFunction.new(@ruby.method(name)) + elsif @ruby.instance_variables.include?(var_name = "@#{name}") + var_value = @ruby.instance_variable_get(var_name) + return Rhino::To.to_javascript(var_value, self) end end - - def has(name, start) - rb_name = name.gsub(/([a-z])([A-Z])/) {"#{$1}_#{$2.downcase}"}.to_sym - To.ruby(start).public_methods(false).collect(&:to_sym).include?(rb_name) ? true : super(name,start) - end - - Generic = new + super end + + # override boolean Scriptable#has(String name, Scriptable start); + # override boolean Scriptable#has(int index, Scriptable start); + def has(name, start) + if name.is_a?(String) + if @ruby.respond_to?(name) || + @ruby.instance_variables.include?("@#{name}") + return true + end + end + super + end + + # override void Scriptable#put(String name, Scriptable start, Object value); + # override void Scriptable#put(int index, Scriptable start, Object value); + def put(name, start, value) + if name.is_a?(String) + if @ruby.respond_to?(set_name = "#{name}=") + return @ruby.send(set_name, Rhino::To.to_ruby(value)) + end + end + super + end + + # override boolean Scriptable#hasInstance(Scriptable instance); + def hasInstance(instance) + super + end + + # override Object[] Scriptable#getIds(); + def getIds + ids = @ruby.instance_variables.map { |ivar| ivar[1..-1].to_java } + @ruby.public_methods(false).each do |name| + name = name[0...-1] if name[-1, 1] == '=' # 'foo=' ... 'foo' + name = name.to_java + ids << name unless ids.include?(name) + end + super.each { |id| ids.unshift(id) } + ids.to_java + end + end end diff --git a/lib/rhino/wormhole.rb b/lib/rhino/wormhole.rb index 8cd57c0..dfe1534 100644 --- a/lib/rhino/wormhole.rb +++ b/lib/rhino/wormhole.rb @@ -29,6 +29,10 @@ module Rhino end def javascript(object, scope = nil); to_javascript(object, scope); end # alias + def args_to_ruby(args) + args.map { |arg| to_ruby(arg) } + end + def array_to_ruby(js_array) js_array.length.times.map { |i| to_ruby( js_array.get(i, js_array) ) } end diff --git a/spec/rhino/context_spec.rb b/spec/rhino/context_spec.rb index 586cec2..c5a17c7 100644 --- a/spec/rhino/context_spec.rb +++ b/spec/rhino/context_spec.rb @@ -25,7 +25,7 @@ describe Rhino::Context do it "allows you to scope the context to an object" do class MyScope - def foo; proc { 'bar' }; end + def foo; 'bar'; end end Rhino::Context.open(:with => MyScope.new) do |ctx| ctx.eval("foo()").should == 'bar' diff --git a/spec/rhino/ruby_function_spec.rb b/spec/rhino/ruby_function_spec.rb new file mode 100644 index 0000000..c0d8336 --- /dev/null +++ b/spec/rhino/ruby_function_spec.rb @@ -0,0 +1,40 @@ +require File.expand_path('../spec_helper', File.dirname(__FILE__)) + +describe Rhino::RubyFunction do + + it "create and unwrap ruby function" do + rb_function = Rhino::RubyFunction.new method = Object.new.method(:to_s) + rb_function.unwrap.should be(method) + end + + it "call a ruby function" do + rb_function = Rhino::RubyFunction.new method = 'foo'.method(:to_s) + context = nil; scope = nil; this = nil; args = nil + rb_function.call(context, scope, this, args).should == 'foo' + end + + it "args get converted before delegating a ruby function call" do + klass = Class.new(Object) do + def foo(array) + array.all? { |elem| elem.is_a?(String) } + end + end + rb_function = Rhino::RubyFunction.new method = klass.new.method(:foo) + context = nil; scope = nil; this = nil + args = [ '1'.to_java, java.lang.String.new('2') ].to_java + args = [ Rhino::JS::NativeArray.new(args) ].to_java + rb_function.call(context, scope, this, args).should be(true) + end + + it "returned value gets converted to javascript" do + klass = Class.new(Object) do + def foo + [ 42 ] + end + end + rb_function = Rhino::RubyFunction.new method = klass.new.method(:foo) + context = nil; scope = nil; this = nil; args = [].to_java + rb_function.call(context, scope, this, args).should be_a(Rhino::JS::NativeArray) + end + +end \ No newline at end of file diff --git a/spec/rhino/ruby_object_spec.rb b/spec/rhino/ruby_object_spec.rb new file mode 100644 index 0000000..892cdc8 --- /dev/null +++ b/spec/rhino/ruby_object_spec.rb @@ -0,0 +1,123 @@ +require File.expand_path('../spec_helper', File.dirname(__FILE__)) + +describe Rhino::RubyObject do + + it "unwraps a ruby object" do + rb_object = Rhino::RubyObject.new object = Object.new + rb_object.unwrap.should be(object) + end + + class UII < Object + end + + it "returns the ruby class name" do + rb_object = Rhino::RubyObject.new UII.new + rb_object.getClassName.should == UII.name + end + + it "reports being a ruby object on toString" do + rb_object = Rhino::RubyObject.new UII.new + rb_object.toString.should == '[ruby UII]' + end + + class UII + + attr_reader :anAttr0 + attr_accessor :the_attr_1 + + def initialize + @anAttr0 = nil + @the_attr_1 = 'attr_1' + @an_attr_2 = 'an_attr_2' + end + + def theMethod0; nil; end + + def a_method1; 1; end + + def the_method_2; '2'; end + + end + + it "gets methods and instance variables" do + rb_object = Rhino::RubyObject.new UII.new + + rb_object.get('anAttr0', nil).should be_a(Rhino::RubyFunction) + rb_object.get('the_attr_1', nil).should be_a(Rhino::RubyFunction) + rb_object.get('an_attr_2', nil).should == 'an_attr_2' + + [ 'theMethod0', 'a_method1', 'the_method_2' ].each do |name| + rb_object.get(name, nil).should be_a(Rhino::RubyFunction) + end + + rb_object.get('non-existent-method', nil).should be(Rhino::JS::Scriptable::NOT_FOUND) + end + + it "has methods and instance variables" do + rb_object = Rhino::RubyObject.new UII.new + + rb_object.has('anAttr0', nil).should be_true + rb_object.has('the_attr_1', nil).should be_true + rb_object.has('an_attr_2', nil).should be_true + + [ 'theMethod0', 'a_method1', 'the_method_2' ].each do |name| + rb_object.has(name, nil).should be_true + end + + rb_object.has('non-existent-method', nil).should be_false + end + + it "puts using attr writer" do + start = mock('start') + start.expects(:put).never + rb_object = Rhino::RubyObject.new UII.new + + rb_object.put('the_attr_1', start, 42) + rb_object.the_attr_1.should == 42 + end + + it "puts a non-existent attr (delegates to start)" do + start = mock('start') + start.expects(:put).once + rb_object = Rhino::RubyObject.new UII.new + + rb_object.put('nonExistingAttr', start, 42) + end + + it "getIds include ruby class methods" do + rb_object = Rhino::RubyObject.new UII.new + + [ 'anAttr0', 'the_attr_1', 'an_attr_2' ].each do |attr| + rb_object.getIds.to_a.should include(attr) + end + [ 'theMethod0', 'a_method1', 'the_method_2' ].each do |method| + rb_object.getIds.to_a.should include(method) + end + end + + it "getIds include ruby instance methods" do + rb_object = Rhino::RubyObject.new object = UII.new + object.instance_eval do + def foo; 'foo'; end + end + + rb_object.getIds.to_a.should include('foo') + end + + it "getIds include writers as attr names" do + rb_object = Rhino::RubyObject.new object = UII.new + + rb_object.getIds.to_a.should include('the_attr_1') + rb_object.getIds.to_a.should_not include('the_attr_1=') + + object.instance_eval do + def foo=(foo) + 'foo' + end + end + + rb_object.getIds.to_a.should include('foo') + rb_object.getIds.to_a.should_not include('foo=') + end + +end \ No newline at end of file diff --git a/spec/rhino/wormhole_spec.rb b/spec/rhino/wormhole_spec.rb index 2db2b8c..33cb876 100644 --- a/spec/rhino/wormhole_spec.rb +++ b/spec/rhino/wormhole_spec.rb @@ -141,24 +141,22 @@ describe Rhino::To do end end - it "creates a prototype for the object based on its class" do - Class.new.tap do |klass| - klass.class_eval do - def foo(one, two) - "1: #{one}, 2: #{two}" - end - end - - Rhino::To.javascript(klass.new).tap do |o| - o.should be_kind_of(Rhino::RubyObject) - o.prototype.tap do |p| - p.should_not be_nil - p.get("foo", p).should_not be_nil - p.get("toString", p).should_not be_nil - end - end - end - end +# it "creates a prototype for the object based on its class" do +# klass = Class.new do +# def foo(one, two) +# "1: #{one}, 2: #{two}" +# end +# end +# +# Rhino::To.javascript(klass.new).tap do |o| +# o.should be_kind_of(Rhino::RubyObject) +# o.prototype.tap do |p| +# p.should_not be_nil +# p.get("foo", p).should_not be_nil +# p.get("toString", p).should_not be_nil +# end +# end +# end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fd2c289..6224a56 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,2 +1,3 @@ +require 'mocha' require 'rhino' diff --git a/therubyrhino.gemspec b/therubyrhino.gemspec index 68adc1f..3e7d2f4 100644 --- a/therubyrhino.gemspec +++ b/therubyrhino.gemspec @@ -17,5 +17,6 @@ Gem::Specification.new do |s| s.add_development_dependency "rake" s.add_development_dependency "rspec" + s.add_development_dependency "mocha" s.add_development_dependency "jruby-openssl" end