h8 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,54 @@
1
1
  module H8
2
2
  class Context
3
+ # Create new context optionally providing variables hash
3
4
  def initialize timout: nil, **kwargs
4
- set **kwargs
5
+ set_all **kwargs
5
6
  end
6
7
 
7
- def set **kwargs
8
+ # set variables from keyword arguments to this context
9
+ def set_all **kwargs
8
10
  kwargs.each { |name, value|
9
11
  set_var(name.to_s, value)
10
12
  }
11
13
  end
12
14
 
15
+ # Set variable for the context
13
16
  def []= name, value
14
- set name => value
17
+ set_all name => value
15
18
  end
16
19
 
17
- def self.eval script
18
- Context.new.eval script
20
+ # Execute a given script on the current context with optionally limited execution time.
21
+ #
22
+ # @param [Float] timeout if is not 0 then maximum execution time in seconds
23
+ # @return [Value] wrapped object returned by the script
24
+ # @raise [H8::TimeoutError] if the timeout was set and expired
25
+ def eval script, timeout: 0
26
+ _eval script, (timeout*1000).to_i
27
+ end
28
+
29
+ # Execute script in a new context with optionally set vars. @see H8#set_all
30
+ # @return [Value] wrapped object returned by the script
31
+ def self.eval script, **kwargs
32
+ Context.new(** kwargs).eval script
33
+ end
34
+
35
+ # Secure gate for JS to securely access ruby class properties (methods with no args)
36
+ # and methods.
37
+ def self.secure_call instance, method, args=nil
38
+ method = method.to_sym
39
+ begin
40
+ m = instance.public_method(method)
41
+ if m.owner == instance.class
42
+ return m.call(*args) if method[-1] == '='
43
+ if m.arity != 0
44
+ return -> (*args) { m.call *args }
45
+ else
46
+ return m.call *args
47
+ end
48
+ end
49
+ rescue NameError
50
+ end
51
+ H8::Undefined
19
52
  end
20
53
  end
21
54
  end
@@ -2,62 +2,108 @@ module H8
2
2
 
3
3
  # Wrapper for javascript objects.
4
4
  #
5
- # Important: when accessin fields of the object, respond_to? will not work due to
6
- # js notation, instead, check the returned value to be value.undefined?
5
+ # Important: when accessing fields of the object, respond_to? will not work due to
6
+ # js notation, instead, check the returned value (there will be always one) to not to be
7
+ # value.undefined?
8
+ #
9
+ # Precaution! Values are always bounded to the context where they are created. You can not
10
+ # pass values between context, as they often map native Javascript objects. If you need, you
11
+ # can copy, for example, by converting it H8::Value#to_ruby first. Another approach is to
12
+ # use JSON serialization from the script.
7
13
  class Value
8
14
 
9
15
  include Comparable
10
16
 
11
17
  def inspect
12
- "<H8::Value #{to_s}>"
18
+ "<H8::Value #{to_ruby}>"
13
19
  end
14
20
 
15
- # Get js object attribute by its name or index (should be Fixnum instance). It always
21
+ # Get js object attribute by either name or index (should be Fixnum instance). It always
16
22
  # return H8::Value instance, check it to (not) be undefined? to see if there is such attribute
23
+ #
24
+ # @return [H8::Value] instance, which is .undefined? if does not exist
17
25
  def [] name_index
18
26
  name_index.is_a?(Fixnum) ? _get_index(name_index) : _get_attr(name_index)
19
27
  end
20
28
 
21
- # Optimized JS member access. Do not yet support calls!
22
- # use only to get fields
29
+ # Optimized JS member access. Support class members and member functions - will call them
30
+ # automatically. Use val['attr'] to get callable instead
23
31
  def method_missing(method_sym, *arguments, &block)
24
32
  name = method_sym.to_s
25
33
  instance_eval <<-End
26
34
  def #{name} *args, **kwargs
27
35
  res = _get_attr('#{name}')
28
- res.function? ? res.apply(res,*args) : res
36
+ (res.is_a?(H8::Value) && res.function?) ? res.apply(self,*args) : res
29
37
  end
30
38
  End
31
39
  send method_sym, *arguments
32
40
  end
33
41
 
34
- # def each_key
35
- # p eval("Object.keys(this)");
36
- # end
37
- #
38
- # def keys
39
- # cxt = self._context
40
- # cxt['__self'] =
41
- # end
42
-
42
+ # Compare to other object, either usual or another Value instance. Does its best.
43
43
  def <=> other
44
44
  other = other.to_ruby if other.is_a?(H8::Value)
45
45
  to_ruby <=> other
46
46
  end
47
47
 
48
+ # Call javascript function represented by this instance (which should be function())
49
+ # with given (or no) arguments.
50
+ # @return [H8::Value] function return which might be undefined
48
51
  def call *args
49
52
  _call args
50
53
  end
51
54
 
52
- def apply to, *args
53
- _apply to, args
55
+ # Like JS apply: call the value that should be function() bounded to a given object
56
+ # @param [Object] this object to bound call to
57
+ # @param args any arguments
58
+ # @return [H8::Value] result returned by the function which might be undefined
59
+ def apply this, *args
60
+ _apply this, args
54
61
  end
55
62
 
63
+ # Tries to convert JS object to ruby array
64
+ # @raise H8::Error if the JS object is not an array
56
65
  def to_ary
57
66
  raise Error, 'Is not an array' unless array?
58
67
  to_ruby
59
68
  end
60
69
 
70
+ alias :to_a :to_ary
71
+
72
+ # Generate set of keys of the wrapped object
73
+ def keys
74
+ context[:__obj] = self
75
+ Set.new context.eval("(Object.keys(__obj));").to_a
76
+ end
77
+
78
+ # enumerate |key, value| pairs for javascript object attributes
79
+ def each
80
+ return enum_for(:each) unless block_given? # Sparkling magic!
81
+ keys.each { |k|
82
+ yield k, _get_attr(k)
83
+ }
84
+ end
85
+
86
+ # Try to convert javascript object to a ruby hash
87
+ def to_h
88
+ each.reduce({}) { |all, kv| all[kv[0]] = kv[1].to_ruby; all }
89
+ end
90
+
91
+ # Iterate over javascript object keys
92
+ def each_key
93
+ keys.each
94
+ end
95
+
96
+ # @return [Array] values array. does NOT convert values to_ruby()
97
+ def values
98
+ each.reduce([]) { |all, kv| all << kv[1]; all }
99
+ end
100
+
101
+ # iterate over values of the javascript object attributes
102
+ def each_value
103
+ values.each
104
+ end
105
+
106
+ # Tries to convert wrapped JS object to ruby primitive (Fixed, String, Float, Array, Hash)
61
107
  def to_ruby
62
108
  case
63
109
  when integer?
@@ -68,10 +114,49 @@ module H8
68
114
  to_f
69
115
  when array?
70
116
  _get_attr('length').to_i.times.map { |i| _get_index(i).to_ruby }
117
+ when object?
118
+ to_h
71
119
  else
72
- raise Error, "Dont know how to convert H8::Value"
120
+ raise Error, "Dont know how to convert #{self.class}"
73
121
  end
74
122
  end
123
+
124
+ def function? # native method. stub for documentation
125
+ end
126
+
127
+ def object? # native method. stub for documentation
128
+ end
129
+
130
+ def undefined? # native method. stub for documentation
131
+ end
132
+
133
+ def array? # native method. stub for documentation
134
+ end
135
+
136
+ # @return [H8::Context] context to which this value is bounded
137
+ def context # native method. stub for documentation
138
+ end
139
+
140
+ # Convert to Proc instance so it could be used as &block parameter
141
+ # @raises (H8::Error) if !self.function?
142
+ def to_proc
143
+ function? or raise H8::Error, 'JS object is not a function'
144
+ -> (*args) { call *args }
145
+ end
146
+
147
+ def to_str
148
+ to_s
149
+ end
75
150
  end
76
151
 
77
152
  end
153
+
154
+ # Ruby object's t_ruby does nothing (tree conversion optimization)
155
+ class Object
156
+ # It is already a ruby object. Gate objects should override
157
+ # as need
158
+ def to_ruby
159
+ self
160
+ end
161
+ end
162
+
@@ -1,3 +1,3 @@
1
1
  module H8
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -5,7 +5,9 @@ describe 'context' do
5
5
 
6
6
  it 'should create' do
7
7
  cxt = H8::Context.new
8
- cxt.eval("'Res: ' + (2+5);")
8
+ cxt[:one] = 1
9
+ cxt.eval("");
10
+ # cxt.eval("'Res: ' + (2+5);")
9
11
  end
10
12
 
11
13
  it 'should gate simple values to JS context' do
@@ -13,7 +15,7 @@ describe 'context' do
13
15
  cxt[:sign] = '!'
14
16
  res = cxt.eval "foo+' '+bar+sign;"
15
17
  res.should == 'hello world!'
16
- cxt.set one: 101, real: 1.21
18
+ cxt.set_all one: 101, real: 1.21
17
19
  cxt.eval("one + one;").should == 202
18
20
  cxt.eval("real + one;").should == (101 + 1.21)
19
21
  end
@@ -28,12 +30,48 @@ describe 'context' do
28
30
 
29
31
  it 'should not gate H8::Values between contexts' do
30
32
  cxt = H8::Context.new
31
- obj = cxt.eval "('che bel');"
33
+ obj = cxt.eval "({res: 'che bel'});"
34
+ # This should be ok
35
+ cxt[:first] = obj
36
+ res = cxt.eval "first.res + ' giorno';"
37
+ res.should == 'che bel giorno'
38
+ # And that should fail
32
39
  cxt1 = H8::Context.new
33
40
  expect( -> {
34
41
  cxt1[:first] = obj
35
- res = cxt1.eval "first + ' giorno';"
42
+ res = cxt1.eval "first.res + ' giorno';"
36
43
  }).to raise_error(H8::Error)
37
44
  end
38
45
 
46
+ it 'should provide reasonable undefined logic' do
47
+ raise "bad !undefined" if !!H8::Undefined
48
+ H8::Undefined.should_not == true
49
+ H8::Undefined.should == false
50
+ H8::Undefined.should_not == 11
51
+ (H8::Undefined==nil).should == false
52
+ H8::Undefined.should be_undefined
53
+ (!H8::Undefined).should == true
54
+ end
55
+
56
+ it 'should limit script execution time' do
57
+ cxt = H8::Context.new
58
+ # cxt[:print] = -> (*args) { puts args.join(' ')}
59
+ script = <<-End
60
+ var start = new Date();
61
+ var last = null;
62
+ var counter = 0;
63
+ while((last=new Date().getTime()) - start < 1000 ) {
64
+ counter++;
65
+ }
66
+ counter;
67
+ End
68
+ # end
69
+ t = Time.now
70
+ expect( -> {
71
+ c2 = cxt.eval script, timeout: 0.2
72
+ }).to raise_error(H8::TimeoutError)
73
+ (Time.now - t).should < 0.25
74
+ cxt.eval('(last-start)/1000').should < 250
75
+ end
76
+
39
77
  end
@@ -1,5 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'h8'
3
+ require 'weakref'
3
4
 
4
5
  describe 'js_gate' do
5
6
 
@@ -18,26 +19,25 @@ describe 'js_gate' do
18
19
  res.should_not be_integer
19
20
  res = cxt.eval("11+22;")
20
21
  res.to_i.should == 33
21
- res.should be_integer
22
- res.should be_float
23
- res.should_not be_string
22
+ res.should be_kind_of(Fixnum)
24
23
  end
25
24
 
26
25
  it 'should return floats' do
27
26
  cxt = H8::Context.new
28
27
  res = cxt.eval("0.2 + 122.1;")
29
28
  res.to_s.should == '122.3'
30
- res.should be_float
31
- res.should_not be_integer
32
- res.should_not be_string
29
+ res.should be_kind_of(Float)
33
30
  end
34
31
 
35
32
  it 'should return strings' do
36
33
  res = H8::Context.eval("'hel' + 'lo';")
37
34
  res.to_s.should == 'hello'
38
- res.should be_string
39
- res.should_not be_integer
40
- res.should_not be_float
35
+ res.should be_kind_of(String)
36
+ end
37
+
38
+ it 'should return undefined and null' do
39
+ H8::Context.eval('undefined').should == H8::Undefined
40
+ H8::Context.eval('null').should == nil
41
41
  end
42
42
 
43
43
  it 'should retreive JS fieds as indexes' do
@@ -46,7 +46,7 @@ describe 'js_gate' do
46
46
  res['bar'].to_i.should == 122
47
47
  end
48
48
 
49
- it 'should retreive JS fieds as properties' do
49
+ it 'should retreive JS fields as properties' do
50
50
  res = H8::Context.eval("({ 'foo': 'bar', 'bar': 122 });")
51
51
  res.bar.to_i.should == 122
52
52
  res.foo.to_s.should == 'bar'
@@ -55,6 +55,9 @@ describe 'js_gate' do
55
55
  # cached method check
56
56
  res.foo.to_s.should == 'bar'
57
57
  res.bar.to_i.should == 122
58
+
59
+ res.bad.should == H8::Undefined
60
+ # res.bad.bad ?
58
61
  end
59
62
 
60
63
  it 'should access arrays' do
@@ -67,11 +70,16 @@ describe 'js_gate' do
67
70
  res[1].to_s.should == 'foo'
68
71
  res[2].to_s.should == 'bar'
69
72
  }
73
+ res[3].should == H8::Undefined
70
74
  end
71
75
 
72
76
  it 'should eval and keep context alive' do
73
- obj = H8::Context.eval("({ 'foo': 'bar', 'bar': 122 });")
74
- GC.start # Here Context of obj that is not referenced should be kept
77
+ cxt = H8::Context.new
78
+ wr = WeakRef.new cxt
79
+ obj = cxt.eval("({ 'foo': 'bar', 'bar': 122 });")
80
+ cxt = nil
81
+ GC.start # cxt is now kept only by H8::Value obj
82
+ wr.weakref_alive?.should be_true
75
83
  obj.foo.should == 'bar'
76
84
  end
77
85
 
@@ -94,13 +102,21 @@ describe 'js_gate' do
94
102
  res = H8::Context.eval("[-10, 'foo', 'bar'];")
95
103
  res.to_ruby.should == [-10, 'foo', 'bar']
96
104
  res.to_ary.should == [-10, 'foo', 'bar']
97
- expect(-> { H8::Context.eval("'not array'").to_ary}).to raise_error(H8::Error)
98
105
  end
99
106
 
100
107
  it 'should provide hash methods' do
101
- pending
102
108
  obj = H8::Context.eval("({ 'foo': 'bar', 'bar': 122 });")
103
- obj.keys.should == ['foo', 'bar']
109
+ obj.keys.should == Set.new(['foo', 'bar'])
110
+
111
+ hash = {}
112
+ obj.each { |k, v| hash[k] = v.to_ruby }
113
+ hash.should == { "foo" => "bar", "bar" => 122 }
114
+ obj.to_h.should == { "foo" => "bar", "bar" => 122 }
115
+ obj.to_ruby.should == { "foo" => "bar", "bar" => 122 }
116
+
117
+ Set.new(obj.each_key).should == Set.new(['foo', 'bar'])
118
+ Set.new(obj.values.map(&:to_ruby)).should == Set.new(['bar', 122])
119
+ Set.new(obj.each_value.map(&:to_ruby)).should == Set.new(['bar', 122])
104
120
  end
105
121
 
106
122
  it 'should convert compare to ruby objects' do
@@ -137,7 +153,7 @@ describe 'js_gate' do
137
153
  end
138
154
 
139
155
  it 'should raise error on syntax' do
140
- expect( -> {
156
+ expect(-> {
141
157
  H8::Context.eval 'this is not a valid js'
142
158
  }).to raise_error(H8::Error)
143
159
  end
@@ -159,4 +175,67 @@ describe 'js_gate' do
159
175
  res.doAdd(10, 1).should == 111
160
176
  end
161
177
 
178
+ it 'should pass exceptions from member function calls' do
179
+ res = H8::Context.eval <<-End
180
+ function cls(base) {
181
+ this.base = base;
182
+ this.doAdd = function(a, b) {
183
+ throw Error("Test error")
184
+ return a + b + base;
185
+ }
186
+ }
187
+ new cls(100);
188
+ End
189
+ expect(-> {
190
+ res.doAdd(10, 1).should == 111
191
+ }).to raise_error(H8::JsError) { |e|
192
+ e.message.should == "Uncaught Error: Test error"
193
+ }
194
+ end
195
+
196
+ it 'should call js functions' do
197
+ res = H8::Context.eval <<-End
198
+ var fn = function cls(a, b) {
199
+ return a + ":" + b;
200
+ }
201
+ fn;
202
+ End
203
+ res.call('foo', 'bar').should == 'foo:bar'
204
+
205
+ def xx(val, &block)
206
+ "::" + val + "-" + block.call('hello', 'world')
207
+ end
208
+
209
+ xx("123", &res.to_proc).should == "::123-hello:world"
210
+ end
211
+
212
+ it 'should gate uncaught exceptions from js callbacks' do
213
+ res = H8::Context.eval <<-End
214
+ var fn = function cls(a, b) {
215
+ throw Error("the test error");
216
+ }
217
+ fn;
218
+ End
219
+ expect(-> {
220
+ res.call('foo', 'bar').should == 'foo:bar'
221
+ }).to raise_error(H8::JsError)
222
+ end
223
+
224
+ it 'should pass thru uncaught ruby exceptions from js->ruby callbacks' do
225
+ class MyException < StandardError;
226
+ end;
227
+ cxt = H8::Context.new
228
+ cxt[:bad_call] = -> { raise MyException }
229
+ res = cxt.eval <<-End
230
+ var fn = function cls(a, b) {
231
+ bad_call();
232
+ }
233
+ fn;
234
+ End
235
+ expect(-> {
236
+ res.call('foo', 'bar').should == 'foo:bar'
237
+ }).to raise_error(MyException)
238
+ end
239
+
240
+
162
241
  end