h8 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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