bellite 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in bellite.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Ivan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Bellite
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'bellite'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install bellite
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bellite.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/bellite/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Ivan"]
6
+ gem.email = ["asmer@asmer.org.ua"]
7
+ gem.description = "Bellite JSON-RPC Client library"
8
+ gem.summary = "Implements connection to JSON-RPC server, calling remote methods and bindings to server events"
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "bellite"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Bellite::VERSION
17
+ end
@@ -0,0 +1,3 @@
1
+ module Bellite
2
+ VERSION = "0.0.1"
3
+ end
data/lib/bellite.rb ADDED
@@ -0,0 +1,729 @@
1
+ #!/usr/bin/ruby
2
+ require 'json'
3
+ require 'socket'
4
+ include Socket::Constants
5
+
6
+
7
+ # Clone of {http://docs.python.org/2/library/functools.html#functools.partial Python functools.partial}
8
+ # Creates a lambda with passed func and applies to it args. Args can be overriden when this lambda called.
9
+ #
10
+ #@param [Proc] func - func to be called with partial parameters
11
+ #@param [Array] *args - default parameters which should be passed to func
12
+ #@return [Proc] wrapper around func which passes *args or new args, passed to wrapper to func
13
+ def partial(func, *args)
14
+ return Proc.new do |*new_args|
15
+ merged_params = args.clone()
16
+ new_args.each_with_index {|v,k|
17
+ merged_params[k] = new_args[k]
18
+ }
19
+ func.call(*merged_params)
20
+ end
21
+ end
22
+
23
+
24
+ # Simple implementation of {http://docs.python.org/2/library/asyncore.html Python Asyncore}
25
+ class Async
26
+ #!attribute @@map
27
+ # List of objects, implements asyncore methods
28
+ @@map = []
29
+
30
+ #Checks sockets from map objects using {IO.select}
31
+ #@param [Float] timeout timeout in seconds, passed to {IO.select}
32
+ #@param [Hash] map list of objects implements asyncore methods
33
+ def Async.check(timeout=false, map=false)
34
+ if not map
35
+ map = @@map
36
+ end
37
+ readable = []
38
+ writable = []
39
+ excepted = []
40
+
41
+ changedCount = 0
42
+ map.each do |obj|
43
+ if obj.writable?
44
+ if obj.fileno
45
+ writable << obj.fileno
46
+ end
47
+ end
48
+
49
+ if obj.readable?
50
+ if obj.fileno
51
+ readable << obj.fileno
52
+ end
53
+ end
54
+
55
+ if obj.exceptable?
56
+ if obj.fileno
57
+ excepted << obj.fileno
58
+ end
59
+ end
60
+ end
61
+
62
+ if readable.size == 0 and writable.size == 0 and excepted.size == 0
63
+ return false
64
+ end
65
+
66
+ if (timeout)
67
+ r,w,e = IO.select(readable, writable, excepted,timeout)
68
+ else
69
+ r,w,e = IO.select(readable, writable, excepted)
70
+ end
71
+ map.each do |obj|
72
+ if obj.writable? and w.include? obj.fileno
73
+ obj.handle_write_event
74
+ end
75
+ if obj.readable? and r.include? obj.fileno
76
+ obj.handle_read_event
77
+ end
78
+ if obj.exceptable? and e.include? obj.fileno
79
+ obj.handle_expt_event
80
+ end
81
+ end
82
+
83
+ return r.size + w.size + e.size
84
+ end
85
+
86
+ #Running check while at least one socket in objects map is connected
87
+ #@param [Float] timeout - timeout, passed to {.check}
88
+ #@param [Hash] map, passed to {.check}
89
+ def Async.loop(timeout,map=false)
90
+ while Async.check(timeout, map) != false
91
+ end
92
+ end
93
+ end
94
+
95
+
96
+
97
+ # Implements new pythonic {http://docs.python.org/2/library/stdtypes.html#dict.setdefault setdefault} method in ruby class Hash
98
+ class Hash
99
+ #(see {Hash})
100
+ #@param key key of hash
101
+ #@param value value of key in hash
102
+ #@return [Value] if key in hash - hash value, attached to key; otherwise value, passed to setdefault
103
+ def setdefault(key, value)
104
+ if self[key] == nil
105
+ self[key] = value
106
+ end
107
+ return self[key]
108
+ end
109
+ end
110
+
111
+
112
+ # @abstract This is common interface of Bellite json-rpc API
113
+ class BelliteJsonRpcApi
114
+ #Constructor. Connects to server with cred credentials
115
+ #@param [String] cred Credentials in format: 'host:port/token';
116
+ def initialize(cred)
117
+ cred = findCredentials(cred)
118
+ if cred
119
+ _connect(cred)
120
+ end
121
+ end
122
+
123
+ #Authenticates with server using token
124
+ #@param [String] token Token to access server
125
+ #@return {Promise}
126
+ def auth(token)
127
+ return _invoke('auth', [token])
128
+ end
129
+
130
+ #Server version
131
+ #@return {Promise}
132
+ def version
133
+ return _invoke('version')
134
+ end
135
+
136
+ #Ping server
137
+ #@return {Promise}
138
+ def ping
139
+ return _invoke('ping')
140
+ end
141
+
142
+ #invokes respondsTo
143
+ #@param [Fixnum] selfId Id for internal use (events, for example)
144
+ #@param [Hash] cmd - some data, which can be encoded as JSON to pass to server
145
+ #@return {Promise}
146
+ def respondsTo(selfId, cmd)
147
+ if not selfId
148
+ selfId = 0
149
+ end
150
+ return _invoke('respondsTo', [selfId, cmd])
151
+ end
152
+
153
+ #perform JSON-RPC request
154
+ #@param [String] cmd command
155
+ #@param [Array] *args Variable-length arguments, passed with cmd. :key => value ruby Hash parameter sugar also works
156
+ #@return [Promise]
157
+ def perform(selfId, cmd, *args)
158
+ if args.size > 1
159
+ args.each do |arg|
160
+ if arg.instance_of(Hash)
161
+ raise ArgumentError, "Cannot specify both positional and keyword arguments"
162
+ end
163
+ end
164
+ end
165
+ if args.size == 1 and (args[0].instance_of? Hash or args[0].instance_of? Array)
166
+ args = args[0]
167
+ end
168
+ if args == []
169
+ args = nil
170
+ end
171
+ if not selfId
172
+ selfId = 0
173
+ end
174
+ return _invoke('perform',[selfId, cmd, args])
175
+ end
176
+
177
+ # binds Event on some evtType
178
+ #@param [Fixnum] selfId internal id
179
+ #@param [String] evtType type of event
180
+ #@param [Fixnum] res
181
+ #@param [Hash] ctx - context, passed to event handler
182
+ #@return [Promise]
183
+ def bindEvent(selfId=0, evtType='*', res = -1, ctx=false)
184
+ if not selfId
185
+ selfId = 0
186
+ end
187
+ return _invoke('bindEvent',[selfId, evtType, res, ctx])
188
+ end
189
+
190
+ # Unbins Event on some evtType
191
+ #@param [Fixnum] selfId internal id
192
+ #@param [String] evtType type of event
193
+ #@return [Promise]
194
+ def unbindEvent(selfId, evtType=false)
195
+ if not selfId
196
+ selfId = 0
197
+ end
198
+ return _invoke('unbindEvent',[selfId, evtType])
199
+ end
200
+
201
+
202
+ # Finds credentials in environment variable BELLITE_SERVER or in passed parameter
203
+ #@param [String] cred server credentials in format host:port/token
204
+ #@return [Hash] with credentials or false if failed
205
+ def findCredentials(cred=false)
206
+ if not cred
207
+ cred = ENV['BELLITE_SERVER']
208
+ if not cred
209
+ cred = '127.0.0.1:3099/bellite-demo-host';
210
+ $stderr.puts 'BELLITE_SERVER environment variable not found, using "'+cred+'"'
211
+ end
212
+ elsif not cred.instance_of String
213
+ return cred
214
+ end
215
+
216
+ begin
217
+ host, token = cred.split('/', 2)
218
+ host, port = host.split(':', 2)
219
+ return ({"credentials" => cred, "token" => token, "host" => host, "port" => Integer(port)})
220
+ rescue
221
+ return false
222
+ end
223
+ end
224
+
225
+ #@abstract Connecting to JSON-RPC server
226
+ #@param [String] host Host
227
+ #@param [Fixnum] port Port
228
+ def _connect(host, port)
229
+ raise NotImplementedError, "Subclass Responsibility"
230
+ end
231
+
232
+ #@abstract Invokes method
233
+ #@param [String] method
234
+ #@param [Hash] params Something JSONable to pass as ethod params
235
+ def _invoke(method, params=nil)
236
+ raise NotImplementedError, "Subclass Responsibility"
237
+ end
238
+ end
239
+
240
+
241
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
242
+ #
243
+
244
+
245
+ #@abstract Next level of Bellite server API implementation: Basic operations with server like Bellite.
246
+ class BelliteJsonRpc < BelliteJsonRpcApi
247
+ #constructor.
248
+ #@param [String] cred Server credentials
249
+ #@param [Boolean] logging Logging flag
250
+ def initialize(cred=false, logging=false)
251
+ @_resultMap = {}
252
+ @_evtTypeMap = {}
253
+ @_nextMsgId = 100
254
+ super (cred)
255
+ @logging = logging
256
+ end
257
+
258
+ #Notify JSON-RPC server by method call, but skip response
259
+ #@param [String] method Method to call
260
+ #@param [Hash,Array,String,Float,Fixnum] params parameters for method
261
+ #@return [Boolean] true if sent
262
+ def _notify(method, params=nil)
263
+ return _sendJsonRpc(method, params)
264
+ end
265
+
266
+ #Calls JSON-RPC method
267
+ #@param [String] method Method to call
268
+ #@param [Hash,Array,String,Float,Fixnum] params parameters for method
269
+ #@return [Promise]
270
+ def _invoke(method, params=nil)
271
+ msgId = @_nextMsgId
272
+ @_nextMsgId += 1
273
+ res = _newResult(msgId)
274
+ _sendJsonRpc(method, params, msgId)
275
+ return res.promise
276
+ end
277
+
278
+ #returns new Promise object, attached to msgId
279
+ #@param [Fixnum] msgId id of message for this result
280
+ #@return [Promise]
281
+ def _newResult(msgId)
282
+ res = deferred()
283
+ @_resultMap[msgId] = res
284
+ return res
285
+ end
286
+
287
+ #Sends JSON-RPC call to server
288
+ #@param [String] method Method to call
289
+ #@param [Hash,Array,String,Float,Fixnum] params parameters for method
290
+ #@param [Fixnum] msgId Message Id. If default, it uses internal autoincrement counter
291
+ #@return [Boolean] True if sent
292
+ def _sendJsonRpc(method, params=nil, msgId=false)
293
+ msg = {"jsonrpc" => "2.0", "method" => method}
294
+ if params
295
+ msg['params'] = params
296
+ end
297
+ if msgId
298
+ msg['id'] = msgId
299
+ end
300
+ logSend(msg)
301
+ return _sendMessage(JSON.fast_generate(msg))
302
+ end
303
+
304
+ #Sends JSON-RPC string to server
305
+ #@param [String] msg JSON-encoded JSON-RPC call to send
306
+ #@return [Boolean] True if sent
307
+ def _sendMessage(msg)
308
+ raise NotImplementedError('Subclass Responsibility')
309
+ end
310
+
311
+ #Puts send packets to STDOUT
312
+ #@param [String] msg Same as for _sendMessage
313
+ def logSend(msg)
314
+ puts "send ==> " + JSON.fast_generate(msg)
315
+ end
316
+
317
+ #Puts received packets to STDOUT
318
+ #@param [String] msg Same as for _sendMessage
319
+ def logRecv(msg)
320
+ puts "recv ==> " + JSON.fast_generate(msg)
321
+ end
322
+
323
+ #Receives JSON-RPC response or call from JSON-RPC Server
324
+ #@param [Array<String>] msgList Array of messages from Server
325
+ def _recvJsonRpc(msgList)
326
+ msgList.each do |msg|
327
+ begin
328
+ msg = JSON.parse(msg)
329
+ isCall = msg.has_key?("method")
330
+ rescue
331
+ next
332
+ end
333
+ logRecv(msg)
334
+ begin
335
+ if isCall
336
+ on_rpc_call(msg)
337
+ else
338
+ on_rpc_response(msg)
339
+ end
340
+ end
341
+ end
342
+ end
343
+
344
+ #Called on JSON-RPC call FROM server.
345
+ #@param [String] msg Server response with call
346
+ def on_rpc_call(msg)
347
+ if msg['method'] == 'event'
348
+ args = msg['params']
349
+ emit(args['evtType'], args)
350
+ end
351
+ end
352
+
353
+ #Called on JSON-RPC response (not call) from Server
354
+ #@param [String] msg Server respon with data
355
+ def on_rpc_response(msg)
356
+ tgt = @_resultMap.delete msg['id']
357
+ if tgt == nil
358
+ return
359
+ end
360
+
361
+ if msg.has_key?('error')
362
+ tgt.reject.call(msg['error'])
363
+ else
364
+ tgt.resolve.call(msg['result'])
365
+ end
366
+ end
367
+
368
+ #Called on connect to remote JSON-RPC server.
369
+ #@param [Hash] cred Server credentials: port, host, token and original `credentials` string
370
+ def on_connect(cred)
371
+ auth(cred['token'])._then.call(method(:on_auth_succeeded), method(:on_auth_failed))
372
+ end
373
+
374
+ #Called when auth is successfully ended
375
+ # Emits 'auth' and 'ready' event handlers
376
+ #@param [String] msg Message from JSON-RPC server
377
+ def on_auth_succeeded(msg)
378
+ emit('auth', true, msg)
379
+ emit('ready')
380
+ end
381
+
382
+ #Called when auth is failed
383
+ # Emits 'auth' event handlers with fail flag
384
+ #@param [String] msg Message from JSON-RPC server
385
+ def on_auth_failed(msg)
386
+ emit('auth', false, msg)
387
+ end
388
+
389
+
390
+ #~ micro event implementation ~~~~~~~~~~~~~~~~~~~~~~~
391
+ #
392
+
393
+ #Adds ready event handler
394
+ #@param [Proc] fnReady Event hanlder lambda
395
+ #@return [Proc] Your event handler
396
+ def ready(fnReady)
397
+ return on('ready', fnReady)
398
+ end
399
+
400
+ #Adds any event handler
401
+ #@param [String] key Event name like `ready`
402
+ #@param [Proc] fn Function to bind on event
403
+ #@return [Proc] Your event handler or bindEvent method to bind your handler later if you skip fn
404
+ def on(key, fn=false)
405
+ bindEvent = lambda do |fn|
406
+ @_evtTypeMap.setdefault(key, []) << fn
407
+ return fn
408
+ end
409
+ if not fn
410
+ return bindEvent
411
+ else
412
+ return bindEvent.call(fn)
413
+ end
414
+ end
415
+
416
+ #Calls event handler when event occured
417
+ #@param [String] key Event name like `ready`
418
+ #@param [Hash,Array,Float,String,Fixnum] *args Argument to pass to event handler(s).
419
+ def emit(key, *args)
420
+ if @_evtTypeMap.has_key? key
421
+ @_evtTypeMap[key].each do |fn|
422
+ begin
423
+ fn.call(self, *args)
424
+ rescue
425
+ puts "EMIT exception"
426
+ end
427
+ end
428
+ end
429
+ end
430
+ end
431
+
432
+
433
+ #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
434
+ #
435
+
436
+
437
+ #Main Bellite class to use
438
+ class Bellite < BelliteJsonRpc
439
+
440
+
441
+ #!attribute [rw] timeout
442
+ # Timeout of IO.select socket wait
443
+ #@return [Float] Timeout value in seconds
444
+ attr_accessor :timeout
445
+
446
+ #Connecting to JSON-RPC server
447
+ # Calls on_connect if connection successfull
448
+ #@param [Hash] cred Server credentials: port, host, token and original `credentials` string
449
+ def _connect(cred)
450
+ @timeout = 0.5
451
+ @conn = TCPSocket.new cred['host'], cred['port']
452
+ @buf = ""
453
+
454
+ if @conn
455
+ on_connect(cred)
456
+ end
457
+ end
458
+
459
+ #Obtains responses from JSON-RPC server
460
+ #@param [Float] timeout Timeout, if false, @_timeout used
461
+ def loop(timeout=0.5)
462
+ if timeout == false
463
+ timeout = @timeout
464
+ end
465
+ Async.loop(timeout, [self])
466
+ end
467
+
468
+ #Sends message to server
469
+ #@param [String] msg JSON-encoded JSON-RPC call to send
470
+ #@return [Boolean] True if sent
471
+ def _sendMessage(msg)
472
+ if not isConnected?
473
+ return false
474
+ end
475
+
476
+ @conn.puts(msg + "\0")
477
+ return true
478
+ end
479
+
480
+ #Checks, is connection still alive
481
+ #@return [Boolean] True if connection alive
482
+ def isConnected?
483
+ return @conn != false
484
+ end
485
+
486
+ #Closing connection to JSON-RPC Server
487
+ def close()
488
+ @conn.close
489
+ @conn = false
490
+ return true
491
+ end
492
+
493
+ #Returns TCPSocket connection for {Async}
494
+ #@return [TCPSocket]
495
+ def fileno()
496
+ return @conn
497
+ end
498
+
499
+ #Returns is this object readable for {Async}
500
+ #@return [Boolean]
501
+ def readable?
502
+ return true
503
+ end
504
+
505
+ #Returns is this object can read out-of-band data for {Async}
506
+ #@return [Boolean]
507
+ def exceptable?
508
+ return false
509
+ end
510
+
511
+ #Reads new data and runs {#_recvJsonRpc}
512
+ # This method called by {Async.loop}
513
+ #@return [Boolean]
514
+ def handle_read_event()
515
+ if not isConnected?
516
+ return false
517
+ end
518
+
519
+ buf = @buf
520
+ begin
521
+ while true
522
+ begin
523
+ part = @conn.recv_nonblock(4096)
524
+ rescue IO::WaitReadable
525
+ break
526
+ end
527
+ if not part
528
+ close()
529
+ break
530
+ elsif part == ""
531
+ break
532
+ else
533
+ buf += part
534
+ end
535
+ end
536
+ rescue
537
+ end
538
+
539
+
540
+ buf = buf.split("\0")
541
+ _recvJsonRpc(buf)
542
+ end
543
+
544
+ #Returns is this object can write data to socket
545
+ # This method called by {Async}
546
+ #@return [Boolean]
547
+ def writable?()
548
+ return false
549
+ end
550
+
551
+ #Writes new data to socket
552
+ # Stub for {Async}
553
+ def handle_write_event()
554
+ end
555
+
556
+ #Handles out-of-band data
557
+ # This method can be called by {Async}. Closes connection if called
558
+ def handle_expt_event()
559
+ close()
560
+ end
561
+
562
+ #Handles socket closing
563
+ # This method can be called by {Async}
564
+ def handle_close()
565
+ close()
566
+ end
567
+
568
+ #Handles socket error
569
+ # This method can be called by {Async}. Closing connection
570
+ def handle_error()
571
+ close()
572
+ end
573
+ end
574
+
575
+ #@abstract Promise API
576
+ class PromiseApi
577
+ #Runs then-function call with same function for success and failure
578
+ #@param [Proc] fn Function to handle success or failure
579
+ #@return Result of then-function call
580
+ def always(fn)
581
+ return @_then.call(fn, fn)
582
+ end
583
+
584
+ #Runs then-function in case of failure
585
+ #@param [Proc] failure Failure handler
586
+ #@return Result of then-function call
587
+ def fail(failure)
588
+ return @_then.call(false, failure)
589
+ end
590
+
591
+ #Runs then-function in case of success
592
+ #@param [Proc] success Success handler
593
+ #@return Result of then-function call
594
+ def done(success)
595
+ return @_then.call(success,false)
596
+ end
597
+ end
598
+
599
+ #Class implements promise/future promise
600
+ class Promise < PromiseApi
601
+ #Constructing object with then-function
602
+ #@param [Proc] _then Then-function
603
+ def initialize(_then)
604
+ if _then
605
+ @_then = _then
606
+ end
607
+ end
608
+
609
+ #Returns this object
610
+ #@return [Promise]
611
+ def promise
612
+ return self
613
+ end
614
+
615
+ #@!attribute [r] _then
616
+ # Then-function
617
+ #@return [Proc,lambda]
618
+ def _then
619
+ @_then
620
+ end
621
+ end
622
+
623
+ #Implements Future
624
+ class Future < PromiseApi
625
+ #Constructing object with then, success and failure functions
626
+ #@param [Proc,lambda] _then Then-function
627
+ #@param [Proc,lambda] resolve Success-function
628
+ #@param [Proc,lambda] reject Failure-function
629
+ def initialize(_then, resolve=false, reject=false)
630
+ @promise = Promise.new _then
631
+ if resolve
632
+ @resolve = resolve
633
+ end
634
+ if reject
635
+ @reject = reject
636
+ end
637
+ end
638
+
639
+
640
+ #@!attribute [r] resolve
641
+ # Success-function
642
+ #@return [Proc,lambda]
643
+ def resolve
644
+ @resolve
645
+ end
646
+
647
+ #@!attribute [r] reject
648
+ # Failure-function
649
+ #@return [Proc,lambda]
650
+ def reject
651
+ @reject
652
+ end
653
+
654
+ #@!attribute [r] promise
655
+ # Promise object of this Future
656
+ #@return [Promise]
657
+ def promise
658
+ @promise
659
+ end
660
+ end
661
+
662
+ #Creates Future object for JSON-RPC Server response
663
+ #@return [Future]
664
+ def deferred()
665
+ cb = []
666
+ answer = false
667
+
668
+ future = false
669
+ reject = false
670
+
671
+ _then = lambda do |*args|
672
+ success = args[0] || false
673
+ failure = args[1] || false
674
+ cb << [success,failure]
675
+ if answer
676
+ answer.call
677
+ end
678
+ return future.promise
679
+ end
680
+
681
+ resolve = lambda do |result|
682
+ while cb.size > 0
683
+ success, failure = cb.pop()
684
+ begin
685
+ if success != false
686
+ res = success.call(result)
687
+ if res != false
688
+ result = res
689
+ end
690
+ end
691
+ rescue Exception => err
692
+ if failure != false
693
+ res = failure.call(err)
694
+ elsif cb.size = 0
695
+ #excepthook
696
+ end
697
+ if res == false
698
+ return reject.call(err)
699
+ else
700
+ return reject.call(res)
701
+ end
702
+ end
703
+ end
704
+ answer = partial(resolve, result)
705
+ end
706
+
707
+ reject = lambda do |error|
708
+ while cb.size > 0
709
+ failure = cb.pop()[1]
710
+ begin
711
+ if failure != false
712
+ res = failure.call(error)
713
+ if res != false
714
+ error = res
715
+ end
716
+ end
717
+ rescue Exception => err
718
+ res = err
719
+ if cb.size == 0
720
+ #excepthook
721
+ end
722
+ end
723
+ end
724
+ answer = partial(reject, error)
725
+ end
726
+
727
+ future = Future.new _then, resolve, reject
728
+ return future
729
+ end
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require '../bellite.rb'
4
+ app = Bellite.new
5
+ app.ready Proc.new {
6
+ puts "READY"
7
+ app.ping
8
+ app.version
9
+ app.perform(142, "echo", {"name" => [nil, true, 42, "value"]})
10
+
11
+ app.bindEvent(118, "*")
12
+ app.unbindEvent(118, "*")
13
+
14
+ app.on("testEvent", lambda { |app, eobj|
15
+ puts "TEST EVENT"
16
+ puts eobj
17
+ if eobj['evt']
18
+ app.perform(0, eobj['evt'])
19
+ else
20
+ app.close
21
+ end
22
+ })
23
+
24
+ app.bindEvent(0, "testEvent", 42, {'testCtx' => true})
25
+ app.perform(0, "testEvent")
26
+ }
27
+
28
+ app.timeout = false
29
+ app.loop false
@@ -0,0 +1,264 @@
1
+ /*-*- coding: utf-8 -*- vim: set ts=4 sw=4 expandtab
2
+ ##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
3
+ ##~ Copyright (C) 2002-2012 Bellite.io ##
4
+ ##~ ##
5
+ ##~ This library is free software; you can redistribute it ##
6
+ ##~ and/or modify it under the terms of the MIT style License as ##
7
+ ##~ found in the LICENSE file included with this distribution. ##
8
+ ##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##*/
9
+
10
+ "use strict";
11
+ function createMockBelliteServer(ns) {
12
+ var self = {
13
+ server: require('net').createServer(),
14
+ token: ns.token || require('crypto').randomBytes(8).toString('hex'),
15
+ shutdownTest: function() {
16
+ self.shutdownTest = null;
17
+ if (self.server)
18
+ self.server.close();
19
+ self.allConnections.forEach(function(conn){
20
+ if (conn) conn.end() }) },
21
+ allConnections: [] }
22
+
23
+ var debugLog = ns.debugLog
24
+
25
+ self.server.on('listening', function () {
26
+ var addr = this.address()
27
+ self.env = addr.address+':'+addr.port+'/'+self.token
28
+ process.env.BELLITE_SERVER = self.env
29
+ if (debugLog) debugLog('Serving BELLITE_SERVER="'+ process.env.BELLITE_SERVER +'"')
30
+ ns.listening();
31
+ })
32
+
33
+ self.server.on('connection', function (conn) {
34
+ self.allConnections.push(conn)
35
+ conn.setEncoding("UTF-8");
36
+ conn.setNoDelay(true);
37
+ conn.setKeepAlive(true, 0);
38
+
39
+ var api = {
40
+ sendMessage: function(msg) {
41
+ if (debugLog) debugLog('reply ==> ', msg);
42
+ return conn.write(msg+'\0') },
43
+ shutdown: function() { return conn.end() },
44
+ fireEvent: function(evtType, selfId, evt, ctx) {
45
+ return this.send('event', {evtType: evtType, selfId: selfId||0, evt:evt, ctx:ctx}) },
46
+ send: function(method, params, id) {
47
+ var msg = {jsonrpc: "2.0", id:id, method:method, params:params}
48
+ return this.sendMessage(JSON.stringify(msg)) },
49
+ answer: function(result, id) {
50
+ if (id===undefined) return false;
51
+ var msg = {jsonrpc: "2.0", id:id, result:result}
52
+ return this.sendMessage(JSON.stringify(msg)) },
53
+ error: function(error, id) {
54
+ if (id===undefined) return false;
55
+ var msg = {jsonrpc: "2.0", id:id, error:error}
56
+ return this.sendMessage(JSON.stringify(msg)) },
57
+ checkAuth: function(token) {
58
+ clearTimeout(authTimeout);
59
+ return this.authorized = (token == self.token) }
60
+ },
61
+ tgt = ns.connect(api),
62
+ authTimeout = setTimeout(api.shutdown, 250);
63
+
64
+ var connBuf='';
65
+ conn.on('data', function(data) {
66
+ data = (connBuf+data).split('\0')
67
+ connBuf = data.pop()
68
+ while (data.length) {
69
+ var msg = data.shift();
70
+ if (debugLog) debugLog('invoke <== ', msg);
71
+ try { msg = JSON.parse(msg) }
72
+ catch (err) { tgt.parse_error(err, msg); continue }
73
+ if (msg.method!==undefined)
74
+ tgt.invoke(msg)
75
+ else tgt.response(msg)
76
+ } })
77
+ conn.on('close', function() { tgt.conn_close() })
78
+ conn.on('error', function(err) { tgt.conn_error(err) })
79
+ })
80
+
81
+ self.server.on('close', function() {
82
+ self.server = null; ns.server_close() })
83
+ self.server.on('error', function(err) {
84
+ ns.server_error(err) })
85
+
86
+ self.server.listen(ns.port || 0, '127.0.0.1')
87
+ return self;
88
+ }
89
+ exports.createMockBelliteServer = createMockBelliteServer;
90
+
91
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
92
+
93
+ function testBelliteJSONRPC(opt, doneCallback) {
94
+ opt.log = opt.log || {};
95
+ function log(key, args) {
96
+ var log=opt.log[key];
97
+ if (!log) opt.log[key] = log = []
98
+ var ea = Array.prototype.slice.call(arguments, 1);
99
+ if (ea.length<2) ea = ea[0]===undefined ? 0 : ea[0];
100
+ log.push(ea)
101
+ return this }
102
+
103
+ var rpc = {
104
+ connect: function(api) {
105
+ log('connect')
106
+ this.api = api;
107
+ this.evtCtx = {}
108
+ return this },
109
+ conn_close: function() { log('conn_close') },
110
+ conn_error: function(err) { log('conn_error', err) },
111
+ parse_error: function(err,msg) { log('parse_error', err, msg) },
112
+ response: function(msg) { log('response', msg) },
113
+ invoke: function(msg) {
114
+ var pre = this.api.authorized ? 'meth_' : 'meth0_';
115
+ var meth = this[pre+msg.method] || this.meth_unknown;
116
+ log('invoke', pre+msg.method, msg.params)
117
+ return meth.call(this, msg.params, msg)},
118
+
119
+ meth_unknown: function(args, msg) {
120
+ log('meth_unknown', msg.method, args)
121
+ this.api.answer(["unknown method ", null], msg.id) },
122
+ meth_ping: function(args, msg) {
123
+ log('meth_ping', args)
124
+ this.api.answer([null, true, "pong"], msg.id) },
125
+ meth_version: function(args, msg) {
126
+ log('meth_version', args)
127
+ this.api.answer([null, {"server":"bellite", "version":"1.4.3", "platform":"node/test"}], msg.id) },
128
+
129
+ meth0_ping: function(args, msg) { this.meth_ping(args, msg) },
130
+ meth0_version: function(args, msg) { this.meth_version(args, msg) },
131
+ meth0_auth: function(args, msg) {
132
+ log('meth_auth', args)
133
+ if (this.api.checkAuth(args[0])) {
134
+ log('authorize', true)
135
+ this.api.answer([null, true, "authorized"], msg.id)
136
+ } else {
137
+ log('authorize', false)
138
+ this.api.error({code:401, message:"Unauthorized"}, msg.id)
139
+ this.api.shutdown()
140
+ } },
141
+
142
+ meth_bindEvent: function(args, msg) {
143
+ log('meth_bindEvent', args)
144
+ this.evtCtx[args[1]] = args[3]
145
+ this.api.answer([null, true], msg.id) },
146
+ meth_unbindEvent: function(args, msg) {
147
+ log('meth_unbindEvent', args)
148
+ this.api.answer([null, true], msg.id) },
149
+ meth_perform: function(args, msg) {
150
+ log('meth_perform', args)
151
+ var self=this, cmd=this['cmd_'+args[1]] || this.cmd_mock
152
+ cmd.call(this, args, function(res) {
153
+ self.api.answer([null, res], msg.id) }) },
154
+
155
+ cmd_mock: function(args, answer) {
156
+ answer([null, {'a mock':'result'}]) },
157
+ cmd_testEvent: function(args, answer) {
158
+ log('cmd_testEvent', args)
159
+ answer([null, true, 'firingTestEvent'])
160
+ var resp = 'dyn_'+require('crypto').randomBytes(4).toString('hex')
161
+ this['cmd_'+resp] = this.dynamic_response
162
+ this.api.fireEvent('testEvent', 0, resp, this.evtCtx.testEvent) },
163
+ dynamic_response: function(args, answer) {
164
+ log('dynamic_response', args)
165
+ answer([null, true, 'awesome'])
166
+ this.api.fireEvent('testEvent', 0, null, this.evtCtx.testEvent) }
167
+ };
168
+
169
+ var test = createMockBelliteServer({
170
+ port: opt.port, token: opt.token, debugLog: opt.debugLog,
171
+ listening: function() {
172
+ opt.execClient(spawnClient) },
173
+ server_close: function() {},
174
+ server_error: function(err) { log('server_error', err) },
175
+ connect: function(api) {
176
+ return Object.create(rpc).connect(api) }
177
+ })
178
+
179
+ function spawnClient(exec, args) {
180
+ var cp = require('child_process')
181
+ test.proc = cp.spawn(exec, args, {stdio:'inherit',cwd:__dirname})
182
+ test.proc.on('exit', function(code, signal) {
183
+ log('process_exit', code, signal)
184
+ test.proc = false;
185
+ done(code!==0 ? 'subprocess spawning error' : null) })
186
+ return test.proc }
187
+
188
+ function done(err) {
189
+ if (done.timer)
190
+ clearTimeout(done.timer)
191
+
192
+ if (test.proc) {
193
+ test.proc.kill()
194
+ test.proc = null
195
+ }
196
+ if (test.shutdownTest) {
197
+ test.shutdownTest()
198
+ setTimeout(function() { doneCallback(err, opt.log, opt) }, 100)
199
+ }}
200
+ if (opt.timeout!==false)
201
+ done.timer = setTimeout(function() {
202
+ log('timeout'); done('timeout') }, opt.timeout || 2000)
203
+ }
204
+ exports.testBelliteJSONRPC = testBelliteJSONRPC;
205
+
206
+ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
207
+
208
+ function assetTestResults(err, log, opt) {
209
+ var assert=require('assert');
210
+ try {
211
+ assert.equal(log.connect && log.connect.length, 1, "should connect exactly once")
212
+ assert.equal(log.server_error, null, "should not experience server errors")
213
+ assert.equal(log.conn_error, null, "should not experience connection errors")
214
+
215
+ assert.equal(log.parse_error, null, "should not experience JSON parsing errors")
216
+ assert.equal(log.response, null, "should not receive JSON-RPC 2.0 responses")
217
+
218
+ assert.equal(log.meth_auth && log.meth_auth.length, 1, "should call auth exactly once")
219
+ assert.deepEqual(log.authorize, [true], "should authorize successfully")
220
+
221
+ assert.equal(log.meth_unknown, null, "should never call an unknown method")
222
+
223
+ assert.deepEqual(log.meth_ping, [0], "should call ping once with no args")
224
+ assert.deepEqual(log.meth_version, [0], "should call version once with no args")
225
+ assert.equal(log.meth_perform && log.meth_perform.length, 3, "should call perform 3 times")
226
+ assert.equal(log.meth_bindEvent && log.meth_bindEvent.length, 2, "should call bindEvent 3 times")
227
+ assert.equal(log.meth_unbindEvent && log.meth_unbindEvent.length, 1, "should call unbindEvent 2 times")
228
+
229
+ assert.deepEqual(log.cmd_testEvent, [[0, 'testEvent', null]], "should call testEvent")
230
+ assert.equal(log.dynamic_response && log.dynamic_response.length, 1, "should call dynamic_response from event")
231
+
232
+ assert.equal(err, null, "terminated with an error")
233
+
234
+ console.log("All Bellite JSON-RPC protocol tests passed")
235
+ process.exit(0) // success
236
+
237
+ } catch(test_err) {
238
+ console.log('\nBellite Mock Server Call Log:')
239
+ console.log(log)
240
+ console.log('\n')
241
+
242
+ console.error(test_err.stack.split(/\n\s*/).slice(0,2).join('\n'))
243
+ console.error(test_err)
244
+ console.error()
245
+
246
+ process.exit(1) // failure
247
+ }
248
+ }
249
+ exports.assetTestResults = assetTestResults;
250
+
251
+ if (!module.parent) {
252
+ // test the bellist JSON-RPC interactions
253
+ testBelliteJSONRPC({
254
+ debugLog: console.log,
255
+ timeout: 2000,
256
+ //timeout: false,
257
+ //port: 3099,
258
+ //token: 'bellite-demo-host',
259
+
260
+ execClient: function(spawn) {
261
+ spawn('ruby', [__dirname+'/_doBelliteTest.rb'])
262
+ }
263
+ }, assetTestResults)
264
+ }
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*- vim: set ts=4 sw=4 expandtab
3
+ ##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
4
+ ##~ Copyright (C) 2002-2012 Bellite.io ##
5
+ ##~ ##
6
+ ##~ This library is free software; you can redistribute it ##
7
+ ##~ and/or modify it under the terms of the MIT style License as ##
8
+ ##~ found in the LICENSE file included with this distribution. ##
9
+ ##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
10
+
11
+ import unittest, os, sys
12
+
13
+ class TestJSONRPC(unittest.TestCase):
14
+ def testBelliteClient(self):
15
+ src = os.path.splitext(__file__)[0]+'.js'
16
+ assert 0==os.system('node '+src)
17
+
18
+
19
+ if __name__=='__main__':
20
+ unittest.main()
21
+
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bellite
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Ivan
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-12-01 00:00:00 Z
19
+ dependencies: []
20
+
21
+ description: Bellite JSON-RPC Client library
22
+ email:
23
+ - asmer@asmer.org.ua
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - .gitignore
32
+ - Gemfile
33
+ - LICENSE
34
+ - README.md
35
+ - Rakefile
36
+ - bellite.gemspec
37
+ - lib/bellite.rb
38
+ - lib/bellite/version.rb
39
+ - test/_doBelliteTest.rb
40
+ - test/testClientJSONRPC.js
41
+ - test/testClientJSONRPC.py
42
+ homepage: ""
43
+ licenses: []
44
+
45
+ post_install_message:
46
+ rdoc_options: []
47
+
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ hash: 3
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ hash: 3
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ requirements: []
69
+
70
+ rubyforge_project:
71
+ rubygems_version: 1.8.24
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: Implements connection to JSON-RPC server, calling remote methods and bindings to server events
75
+ test_files:
76
+ - test/_doBelliteTest.rb
77
+ - test/testClientJSONRPC.js
78
+ - test/testClientJSONRPC.py
79
+ has_rdoc: