lib/js/proxy.rb
require "opal" require "native" module JS module Helpers def wrap_result(result) if `result && typeof result.then === 'function'` Promise.new(result) elsif `typeof result === 'object' && result !== null` Proxy.new(result) else result end end def native_methods @native_methods ||= %x{ let obj = #{to_n}; const props = new Set(); while (obj !== null) { for (const key of Reflect.ownKeys(obj)) { const stringKey = key.toString() const rubyName = #{to_rb_name(`stringKey`)} if (typeof key !== 'symbol' && stringKey !== rubyName ) { props.add(rubyName); } props.add(stringKey); } obj = Object.getPrototypeOf(obj); } return Array.from(props); } end end class Proxy include Enumerable include Helpers attr_accessor :native IRREGULARS = %w(html url uri) def initialize(native) @native = Native(native) end def method_missing(name, *args, &block) js_name = to_js_name(name) unless existing_property?(js_name) || js_name.end_with?("=") raise NoMethodError, "undefined method `#{name}` for #{self}" end if js_name.end_with?("=") prop = js_name[0..-2] native[prop] = args.first else val = native[js_name] if `typeof val === 'function'` js_args = args.dup if block js_callback = %x{ function() { let args = Array.prototype.slice.call(arguments); return #{block.call(self.class.new(`this`), *args)}; } } js_args << js_callback end result = `val.apply(#{to_n}, #{js_args.to_n})` wrap_result(result) elsif `typeof val === 'object' && val !== null` wrap_result(val) else val end end end def to_str `#{to_n}.toString()` end def respond_to_missing?(name, include_private = false) true end def each return enum_for(:each) unless respond_to?(:length) length = self.length (0...length).each do |i| yield self[i] end end def [](index) val = native[index] wrap_result(val) end def []=(k, v) native[k] = v wrap_result(native) end def to_n native.to_n end def length native.length end private def to_js_name(name) name.to_s.split('_').map.with_index do |part, index| if IRREGULARS.include? part.gsub("=", "").downcase part.upcase else index.zero? ? part : part.capitalize end end.join end def to_rb_name(name) name .to_s .gsub(/([A-Z]+)/) { "_#{$1.downcase}" } .sub(/^_/, '') end def existing_property?(property) `#{property} in #{to_n}` end end class Promise < Proxy include Helpers def then(&block) js_callback = %x{ function(value) { var ruby_result = #{block.call(wrap_result(`value`))}; if (ruby_result && typeof ruby_result.then === 'function') { return ruby_result; } else if (ruby_result && typeof ruby_result.to_n === 'function') { return ruby_result.to_n(); } else { return ruby_result; } } } self.native = `#{to_n}.then(#{js_callback})` end def catch(&block) js_callback = %x{ function(error) { var ruby_result = #{block.call(wrap_result(`error`))}; if (ruby_result && typeof ruby_result.then === 'function') { return ruby_result; } else if (ruby_result && typeof ruby_result.to_n === 'function') { return ruby_result.to_n(); } else { return ruby_result; } } } self.native = `#{to_n}.catch(#{js_callback})` end end end