Skip to content

Commit 7d15e01

Browse files
Merge pull request #541 from ledsun/js_object_inherits_basic_object
JS::Object inherits BasicObject
2 parents 4c12c77 + 58e3853 commit 7d15e01

File tree

6 files changed

+74
-16
lines changed

6 files changed

+74
-16
lines changed

packages/gems/js/ext/js/js-core.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ void Init_js() {
580580
rb_define_module_function(rb_mJS, "global", _rb_js_global_this, 0);
581581

582582
i_to_js = rb_intern("to_js");
583-
rb_cJS_Object = rb_define_class_under(rb_mJS, "Object", rb_cObject);
583+
rb_cJS_Object = rb_define_class_under(rb_mJS, "Object", rb_cBasicObject);
584584
VALUE rb_cJS_singleton = rb_singleton_class(rb_cJS_Object);
585585
rb_define_alloc_func(rb_cJS_Object, jsvalue_s_allocate);
586586
rb_define_method(rb_cJS_Object, "[]", _rb_js_obj_aref, 1);

packages/gems/js/lib/js.rb

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,20 @@ def self.__async(future, &block)
125125
end
126126
end
127127

128-
class JS::Object
128+
# Inherit BasicObject to prevent define coventional menthods. #Override the `Object#send` to give priority to `send` method of JavaScript.
129+
#
130+
# This is to make it easier to use JavaScript Objects with `send` method such as `WebSocket` and `XMLHttpRequest`.
131+
# The JavaScript method call short-hand in `JS::Object` is implemented using `method_missing`.
132+
# If JS::Object inherits from Object, the `send` method defined in Ruby will take precedence over the JavaScript `send` method.
133+
# If you want to call the JavaScript `send` method, you must use the `call` method as follows:
134+
#
135+
# ws = JS.global[:WebSocket].new("ws://example.com")
136+
# ws.call(:send, ["Hello, world! from Ruby"])
137+
#
138+
# This inheritation allows you to call the JavaScript `send` method with the following syntax:
139+
#
140+
# ws.send("Hello, world! from Ruby")
141+
class JS::Object < BasicObject
129142
# Create a JavaScript object with the new method
130143
#
131144
# The below examples show typical usage in Ruby
@@ -141,16 +154,16 @@ class JS::Object
141154
#
142155
def new(*args, &block)
143156
args = args + [block] if block
144-
JS.global[:Reflect].construct(self, args.to_js)
157+
::JS.global[:Reflect].construct(self, args.to_js)
145158
end
146159

147160
# Converts +self+ to an Array:
148161
#
149162
# JS.eval("return [1, 2, 3]").to_a.map(&:to_i) # => [1, 2, 3]
150163
# JS.global[:document].querySelectorAll("p").to_a # => [[object HTMLParagraphElement], ...
151164
def to_a
152-
as_array = JS.global[:Array].from(self)
153-
Array.new(as_array[:length].to_i) { as_array[_1] }
165+
as_array = ::JS.global[:Array].from(self)
166+
::Array.new(as_array[:length].to_i) { as_array[_1] }
154167
end
155168

156169
# Provide a shorthand form for JS::Object#call
@@ -176,7 +189,7 @@ def method_missing(sym, *args, &block)
176189
result = invoke_js_method(sym_str[0..-2].to_sym, *args, &block)
177190
# Type coerce the result to boolean type
178191
# to match the true/false determination in JavaScript's if statement.
179-
return JS.global.Boolean(result) == JS::True
192+
return ::JS.global.Boolean(result) == ::JS::True
180193
end
181194

182195
invoke_js_method(sym, *args, &block)
@@ -186,7 +199,6 @@ def method_missing(sym, *args, &block)
186199
#
187200
# See JS::Object#method_missing for details.
188201
def respond_to_missing?(sym, include_private)
189-
return true if super
190202
sym_str = sym.to_s
191203
sym = sym_str[0..-2].to_sym if sym_str.end_with?("?")
192204
self[sym].typeof == "function"
@@ -203,7 +215,7 @@ def respond_to_missing?(sym, include_private)
203215
# end.await # => 42
204216
def apply(*args, &block)
205217
args = args + [block] if block
206-
JS.global[:Reflect].call(:apply, self, JS::Undefined, args.to_js)
218+
::JS.global[:Reflect].call(:apply, self, ::JS::Undefined, args.to_js)
207219
end
208220

209221
# Await a JavaScript Promise like `await` in JavaScript.
@@ -233,8 +245,17 @@ def apply(*args, &block)
233245
# JS.eval("return new Promise((ok, err) => err(new Error())").await # => raises JS::Error
234246
def await
235247
# Promise.resolve wrap a value or flattens promise-like object and its thenable chain
236-
promise = JS.global[:Promise].resolve(self)
237-
JS.promise_scheduler.await(promise)
248+
promise = ::JS.global[:Promise].resolve(self)
249+
::JS.promise_scheduler.await(promise)
250+
end
251+
252+
# The `respond_to?` method is only used in unit tests.
253+
# There is little need to define it here.
254+
# However, methods suffixed with `?` do not conflict with JavaScript methods.
255+
# As there are no disadvantages, we will define the `respond_to?` method here
256+
# in the same way as the `nil?` and `is_a?` methods, prioritizing convenience.
257+
[:nil?, :is_a?, :raise, :respond_to?].each do |method|
258+
define_method(method, ::Object.instance_method(method))
238259
end
239260

240261
private
@@ -246,12 +267,12 @@ def invoke_js_method(sym, *args, &block)
246267
return self.call(sym, *args, &block) if self[sym].typeof == "function"
247268

248269
# Check to see if a non-functional property exists.
249-
if JS.global[:Reflect].call(:has, self, sym.to_s) == JS::True
250-
raise TypeError,
270+
if ::JS.global[:Reflect].call(:has, self, sym.to_s) == ::JS::True
271+
raise ::TypeError,
251272
"`#{sym}` is not a function. To reference a property, use `[:#{sym}]` syntax instead."
252273
end
253274

254-
raise NoMethodError,
275+
raise ::NoMethodError,
255276
"undefined method `#{sym}' for an instance of JS::Object"
256277
end
257278
end

packages/npm-packages/ruby-wasm-wasi/test/unit/test_error.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
require "js"
33

44
class JS::TestError < Test::Unit::TestCase
5+
using JsObjectTestable
6+
57
def test_throw_error
68
e = assert_raise(JS::Error) { JS.eval("throw new Error('foo')") }
79
assert_match /^Error: foo/, e.message

packages/npm-packages/ruby-wasm-wasi/test/unit/test_float.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
require "js"
33

44
class JS::TestFloat < Test::Unit::TestCase
5+
using JsObjectTestable
6+
57
def test_to_js
68
assert_equal (1.0).to_js, JS.eval("return 1.0;")
79
assert_equal (0.5).to_js, JS.eval("return 0.5;")

packages/npm-packages/ruby-wasm-wasi/test/unit/test_object.rb

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,9 +368,29 @@ def test_respond_to_missing?
368368
object = JS.eval(<<~JS)
369369
return { foo() { return true; } };
370370
JS
371-
assert_true object.respond_to?(:foo)
372-
assert_true object.respond_to?(:new)
373-
assert_false object.respond_to?(:bar)
371+
assert_true object.__send__(:respond_to_missing?, :foo, false)
372+
assert_false object.__send__(:respond_to_missing?, :bar, false)
373+
374+
# new is method of JS::Object
375+
assert_false object.__send__(:respond_to_missing?, :new, false)
376+
377+
# send is not implemented in JS::Object,
378+
# because JS::Object is a subclass of JS::BaseObject
379+
assert_false object.__send__(:respond_to_missing?, :send, false)
380+
end
381+
382+
def test_send_method_for_javascript_object_with_send_method
383+
object = JS.eval(<<~JS)
384+
return { send(message) { return message; } };
385+
JS
386+
assert_equal "hello", object.send("hello").to_s
387+
end
388+
389+
def test_send_method_for_javascript_object_without_send_method
390+
object = JS.eval(<<~JS)
391+
return { write(message) { return message; } };
392+
JS
393+
assert_raise(NoMethodError) { object.send("hello") }
374394
end
375395

376396
def test_member_get

packages/npm-packages/ruby-wasm-wasi/tools/run-test-unit.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,19 @@ const test = async (instantiate) => {
169169

170170
await vm.evalAsync(`
171171
require 'test/unit'
172+
173+
# FIXME: This is a workaround for the test-unit gem.
174+
# It will be removed when the next pull request is merged and released.
175+
# https://github.com/test-unit/test-unit/pull/262
176+
require 'pp'
177+
module JsObjectTestable
178+
refine JS::Object do
179+
[:object_id, :pretty_inspect].each do |method|
180+
define_method(method, ::Object.instance_method(method))
181+
end
182+
end
183+
end
184+
172185
require_relative '${rootTestFile}'
173186
ok = Test::Unit::AutoRunner.run
174187
exit(1) unless ok

0 commit comments

Comments
 (0)