farcall 0.1.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0590f2273cea0df6aa322c05cc5b5b4b75be5da0
4
- data.tar.gz: be36bff0dfe37581e19af74308361b9958cdc4a2
3
+ metadata.gz: 7ccf7c600e32b64b16b3ee5e278064bef7214274
4
+ data.tar.gz: 2e7fc842321b53483a09b02caf9cc9c8fc8af85f
5
5
  SHA512:
6
- metadata.gz: 4d400f0595d4b54caeff4f8c09eacc5282b5fdf23b5eb57b4c5f5a5620ad82d2e95d7520212b655a0f29a9fc4fd0e79161c2f236fe51f3b28536327c3277e634
7
- data.tar.gz: 680ead9d24db0a9fc2b6b09c2c40c661030ad1917f91ddff132d6c85de92625ea269e02ec44616be135decdac714067e8e77050374a08720917bf53a52bb4a52
6
+ metadata.gz: 611fea3359ae86917d1625f8b1daf79e78c4cf20421a8ee64d3e679a00fbe0007a8bd79a551fb9e24bf6d38e3eea47dc9e77ac92927cb05c7c66f51d0ae8749e
7
+ data.tar.gz: 4010ab7cf9dbe52c2e4ca519c3df823a5ea104ff7783118340e576ebd2551c0866b20967c55b82e0a8a23e00a7314a73362efb433db6da9f092a8386c1388afe
data/README.md CHANGED
@@ -1,11 +1,23 @@
1
1
  # Farcall
2
2
 
3
+ ## News
4
+
5
+ Since 0.3.0 farcall gem provides websocket client and server out of the box.
6
+
3
7
  ## Description
4
8
 
5
9
  The simple and elegant cross-platform RPC protocol that uses any formatter/transport capable of
6
10
  transmitting dictionary-like objects, for example, JSON,
7
11
  [BOSS](https://github.com/sergeych/boss_protocol), XML, BSON and many others. This gem
8
- supports out of the box JSON and [BOSS](https://github.com/sergeych/boss_protocol) protocols.
12
+ supports out of the box JSON and [BOSS](https://github.com/sergeych/boss_protocol) protocols and
13
+ streams and sockets as the media.
14
+
15
+ There is also optional support for eventmachine based wbesocket server and regular client websocket
16
+ connection. All you need is to include gem 'em-websocket' and/or gem 'websocket-client-simple'.
17
+ All websocket implementations use JSON encoding to vbe interoperable with most web allications.
18
+
19
+ We do not include them in the dependencies because eventmachine is big and does not work with jruby,
20
+ and websocket client is not always needed and we are fond of minimizing dependencies.
9
21
 
10
22
  RPC is made asynchronously, each call can have any return values. While one call is waiting,
11
23
  other calls can be executed. The protocol is bidirectional Call parameters could be
@@ -15,7 +27,7 @@ dictionary, wahtever.
15
27
  Exception/errors transmitting is also supported. The interface is very simple and rubyish. The
16
28
  protocol is very easy to implement if there is no implementation, see
17
29
  [Farcall protocol specification](https://github.com/sergeych/farcall/wiki). Java library for
18
- Android and desktop is coming soon.
30
+ Android and desktop is ready upon request (leave me a task or a message in th github).
19
31
 
20
32
  ## Installation
21
33
 
@@ -23,8 +35,15 @@ Add this line to your application's Gemfile:
23
35
 
24
36
  ```ruby
25
37
  gem 'farcall'
26
- # If you want to use binary-effective boss encoding:
27
- # gem 'noss-protocol', '>= 1.4.1'
38
+ # If you want to use binary-effective boss encoding, uncomment:
39
+ # gem 'boss-protocol', '>= 1.4.1'
40
+ #
41
+ # if you want to use eventmachine and server websocket, uncomment:
42
+ # gem 'em-websocket'
43
+ #
44
+ # To use websocket client, uncomment
45
+ # gem 'websocket-client-simple'
46
+
28
47
  ```
29
48
 
30
49
  And then execute:
@@ -59,8 +78,16 @@ Or install it yourself as:
59
78
  TestProvider.new socket: connected_socket, format: :boss
60
79
  ```
61
80
 
62
- Suppose whe have some socket connected to one above, then TestProvider methods are available via
63
- this connection:
81
+ `Farcall::Provider` provides easy constructors to use it with the transport or the endpoint.
82
+ If you need to implement farcall over somw other media, just extend `TestTransport` and provide
83
+ `send_data` and call `on_received_data` when need. It's very simple and straightforward.
84
+
85
+ Consult [online documentation for Transport](http://www.rubydoc.info/gems/farcall/Farcall/Transport)
86
+ and [Provider](http://www.rubydoc.info/gems/farcall/Farcall/Provider) for more.
87
+
88
+ In the most common case you just have to connect two sockets, in which case everythng works right
89
+ out of the box. Suppose whe have some socket connected to one above, then TestProvider methods are
90
+ available via this connection:
64
91
 
65
92
  ```ruby
66
93
 
@@ -120,11 +147,15 @@ So, I would recommend:
120
147
 
121
148
  - if you need BOSS but can't find it on your platform, use both and contact me :)
122
149
 
150
+ ## Usage with eventmacine
151
+
152
+ You can use `EmFarcall::Endpoint` widely as it uses a pair of EM::Channel as a trasnport, e.g.
153
+ it could be easily connected to any evented data source.
154
+
123
155
  ## Documentation
124
156
 
125
157
  * [Farcall protocol](https://github.com/sergeych/farcall/wiki)
126
158
 
127
-
128
159
  * Gem [online docs](http://www.rubydoc.info/gems/farcall)
129
160
 
130
161
  ## Contributing
data/farcall.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
  pass structures (dictionaries, hashes, whatever you name it). Out of the box provides
15
15
  JSON and BOSS formats over streams and sockets.
16
16
  End
17
- spec.homepage = ""
17
+ spec.homepage = "https://github.com/sergeych/farcall"
18
18
  spec.license = "MIT"
19
19
 
20
20
  spec.files = `git ls-files -z`.split("\x0")
@@ -22,8 +22,11 @@ Gem::Specification.new do |spec|
22
22
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_development_dependency "bundler", "~> 1.7"
26
- spec.add_development_dependency "rake", "~> 10.0"
27
- spec.add_development_dependency "rspec"
28
- spec.add_development_dependency "boss-protocol", ">= 1.4.1"
25
+ spec.add_dependency 'hashie'
26
+ spec.add_development_dependency 'bundler', '~> 1.7'
27
+ spec.add_development_dependency 'rake', '~> 10.0'
28
+ spec.add_development_dependency 'rspec'
29
+ spec.add_development_dependency 'em-websocket'
30
+ spec.add_development_dependency 'websocket-client-simple'
31
+ spec.add_development_dependency 'boss-protocol', '>= 1.4.3'
29
32
  end
@@ -9,6 +9,8 @@ module Farcall
9
9
  # Create json transport, see Farcall::Transpor#create for parameters
10
10
  def initialize **params
11
11
  setup_streams **params
12
+ @formatter = Boss::Formatter.new(@output)
13
+ @formatter.set_stream_mode
12
14
  end
13
15
 
14
16
  def on_data_received= block
@@ -21,7 +23,7 @@ module Farcall
21
23
  end
22
24
 
23
25
  def send_data hash
24
- @output << Boss.dump(hash)
26
+ @formatter << hash
25
27
  end
26
28
 
27
29
  def close
@@ -0,0 +1,248 @@
1
+ begin
2
+ require 'hashie'
3
+ require 'eventmachine'
4
+
5
+ # As the eventmachine callback paradigm is completely different from the threaded paradigm
6
+ # of the Farcall, that runs pretty well under JRuby and in multithreaded MRI, we provide
7
+ # compatible but different implementations: {EmFarcall::Endpoint}, {EmFarcall::Interface}
8
+ # and {EmFarcall::Provider}. Changes to adapt these are minimal except of the callback
9
+ # paradigm. The rest is the same.
10
+ #
11
+ module EmFarcall
12
+
13
+ # Endpoint that run in the reactor thread of the EM. Eventmachine should rin by the time of
14
+ # creation of the endpoint. All the methods can be called from any thread, not only
15
+ # EM's reactor thread.
16
+ #
17
+ # As the eventmachine callback paradigm is completely different from the threaded paradigm
18
+ # of the Farcall, that runs pretty well under
19
+ # JRuby and in multithreaded MRI, we provide
20
+ # compatible but different endpoint to run under EM.
21
+ #
22
+ # Its main difference is that there is no sync_call, instead, calling remote commands
23
+ # from the endpoint and/ot interface can provide blocks that are called when the remote
24
+ # is executed.
25
+ #
26
+ # The EM version of the endpoint works with any 2 EM:C:Channels.
27
+ #
28
+ class Endpoint
29
+
30
+ # Create new endpoint to work with input and output channels
31
+ #
32
+ # @param [EM::Channel] input_channel
33
+ # @param [EM::Channel] output_channel
34
+ def initialize(input_channel, output_channel, errback=nil, provider: nil)
35
+ EM.schedule {
36
+ @input, @output, @errback = input_channel, output_channel, errback
37
+ @trace = false
38
+ @in_serial = @out_serial = 0
39
+ @callbacks = {}
40
+ @handlers = {}
41
+ @unnamed_handler = -> (name, *args, **kwargs) {
42
+ raise NoMethodError, "method does not exist: #{name}"
43
+ }
44
+ @input.subscribe { |data|
45
+ process_input(data)
46
+ }
47
+ if provider
48
+ @provider = provider
49
+ provider.endpoint = self
50
+ end
51
+ }
52
+ end
53
+
54
+ # Set or get provider instance. When provider is set, its public methods are called by the remote
55
+ # and any possible exception are passed back to caller party. You can use any ruby class instance
56
+ # everything will work, operators, indexes[] and like.
57
+ attr_accessor :provider
58
+
59
+ # Call remote with specified name and arguments calling block when done.
60
+ # if block is provided, it will be called when the remote will be called and possibly return
61
+ # some data.
62
+ #
63
+ # Block receives single object paramter with two fields: `result.error` and `result.result`.
64
+ #
65
+ # `result.error` is not nil when the remote raised error, then `error[:class]` and
66
+ # `error.text` are set accordingly.
67
+ #
68
+ # if error is nil then result.result receives any return data from the remote method.
69
+ #
70
+ # for example:
71
+ #
72
+ # endpoint.call( 'some_func', 10, 20) { |done|
73
+ # if done.error
74
+ # puts "Remote error class: #{done.error[:class]}: #{done.error.text}"
75
+ # else
76
+ # puts "Remote returned #{done.result}"
77
+ # }
78
+ #
79
+ # @param [String] name command name
80
+ # @return [Endpoint] self
81
+ def call(name, *args, **kwargs, &block)
82
+ EM.schedule {
83
+ if block
84
+ @callbacks[@in_serial] = block
85
+ end
86
+ send_block cmd: name, args: args, kwargs: kwargs
87
+ }
88
+ self
89
+ end
90
+
91
+ # Close the endpoint
92
+ def close
93
+ super
94
+ end
95
+
96
+ # Report error via errback and the endpoint
97
+ def error text
98
+ STDERR.puts "farcall ws server error #{text}"
99
+ EM.schedule {
100
+ @errback.call(text) if @errback
101
+ close
102
+ }
103
+ end
104
+
105
+ # Set handler to perform the named command. Block will be called when the remote party calls
106
+ # with parameters passed from the remote. The block returned value will be passed back to
107
+ # the caller.
108
+ #
109
+ # If the block raises the exception it will be reported to the caller as an error (depending
110
+ # on it's platofrm, will raise exception on its end or report error)
111
+ def on(name, &block)
112
+ @handlers[name.to_s] = block
113
+ end
114
+
115
+ # Process remote command. First parameter passed to the block is the method name, the rest
116
+ # are optional arguments of the call:
117
+ #
118
+ # endpoint.on_command { |name, *args, **kwargs|
119
+ # if name == 'echo'
120
+ # { args: args, keyword_args: kwargs }
121
+ # else
122
+ # raise "unknown command"
123
+ # end
124
+ # }
125
+ #
126
+ # raising exceptions from the block cause farcall error to be returned back th the caller.
127
+ def on_command &block
128
+ raise "unnamed handler should be present" unless block
129
+ @unnamed_handler = block
130
+ end
131
+
132
+ # Same as #on_command (compatibilty method)
133
+ def on_remote_call &block
134
+ on_command block
135
+ end
136
+
137
+ # Get the Farcall::RemoteInterface connnected to this endpoint. Any subsequent calls with
138
+ # return the same instance.
139
+ def remote
140
+ @remote ||= EmFarcall::Interface.new endpoint: self
141
+ end
142
+
143
+ private
144
+
145
+ # :nodoc: sends block with correct framing
146
+ def send_block **data
147
+ data[:serial] = @out_serial
148
+ @out_serial += 1
149
+ @output << data
150
+ end
151
+
152
+ # :nodoc:
153
+ def execute_command cmd, ref, args, kwargs
154
+ kwargs = (kwargs || {}).inject({}) {
155
+ |all, kv|
156
+ all[kv[0].to_sym] = kv[1]
157
+ all
158
+ }
159
+ args << kwargs if kwargs && !kwargs.empty?
160
+ result = if proc = @handlers[cmd.to_s]
161
+ proc.call(*args)
162
+ elsif @provider
163
+ provider.send :remote_call, cmd.to_sym, args
164
+ else
165
+ @unnamed_handler.call(cmd, args)
166
+ end
167
+ send_block ref: ref, result: result
168
+
169
+ rescue
170
+ if @trace
171
+ puts $!
172
+ puts $!.backtrace.join("\n")
173
+ end
174
+ send_block ref: ref, error: { class: $!.class.name, text: $!.to_s }
175
+ end
176
+
177
+ # :nodoc: important that this method is called from reactor thread only
178
+ def process_input data
179
+ # To be free from :keys and 'keys'
180
+ data = Hashie::Mash.new(data) unless data.is_a?(Hashie::Mash)
181
+ if data.serial != @in_serial
182
+ error "framing error (wrong serial:)"
183
+ else
184
+ @in_serial += 1
185
+ if (cmd = data.cmd) != nil
186
+ execute_command(cmd, data.serial, data.args || [], data.kwargs || {})
187
+ else
188
+ ref = data.ref
189
+ if ref
190
+ if (block = @callbacks.delete(ref)) != nil
191
+ block.call(Hashie::Mash.new(result: data.result, error: data.error))
192
+ end
193
+ else
194
+ error "framing error: no ref in block #{data.inspect}"
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ # Interface to the remote provider via Farcall protocols. Works the same as if the object
202
+ # is local and yields block in return, unlike Farcall::Interface that blocks
203
+ #
204
+ # RemoteInterface transparently creates methods as you call them to speedup subsequent
205
+ # calls.
206
+ #
207
+ class Interface
208
+
209
+ # Create interface connected to some endpoint ar transpost.
210
+ #
211
+ # Please remember that Farcall::Transport instance could be used with only
212
+ # one connected object, unlike Farcall::Endpoint, which could be connected to several
213
+ # consumers.
214
+ #
215
+ # @param [Farcall::Endpoint|Farcall::Transport] arg either endpoint or a transport
216
+ # to connect interface to
217
+ def initialize(endpoint)
218
+ @endpoint = endpoint
219
+ end
220
+
221
+ def method_missing(method_name, *arguments, **kw_arguments, &block)
222
+ instance_eval <<-End
223
+ def #{method_name} *arguments, **kw_arguments, &block
224
+ @endpoint.call '#{method_name}', *arguments, **kw_arguments, &block
225
+ end
226
+ End
227
+ @endpoint.call method_name, *arguments, **kw_arguments, &block
228
+ end
229
+
230
+ def respond_to_missing?(method_name, include_private = false)
231
+ true
232
+ end
233
+ end
234
+
235
+ class Provider < Farcall::Provider
236
+
237
+ attr_accessor :endpoint
238
+
239
+ def far_interface
240
+ endpoint.remote
241
+ end
242
+
243
+ end
244
+ end
245
+ rescue LoadError
246
+ $!.to_s =~ /eventmachine/ or raise
247
+ end
248
+
@@ -0,0 +1,73 @@
1
+ begin
2
+ require 'em-websocket'
3
+ require 'eventmachine'
4
+ require_relative './em_farcall'
5
+
6
+ module EmFarcall
7
+
8
+ # Farcall websocket client. To use it you must add to your Gemfile:
9
+ #
10
+ # gem 'websocket-client-simple'
11
+ #
12
+ # then you can use it with EM:
13
+ #
14
+ # endpoint = nil
15
+ #
16
+ # EM::WebSocket.run(params) do |ws|
17
+ # ws.onopen { |handshake|
18
+ # # Check handshake.path, handshake query
19
+ # # for example to select the provider, then connect Farcall to websocket:
20
+ # endpoint = EmFarcall::WsServerEndpoint.new ws, provider: WsProvider.new
21
+ # }
22
+ # end
23
+ #
24
+ # now we can use it as usual: remote can call provder method, and we can call remote:
25
+ #
26
+ # endpoint.remote.do_something( times: 4) { |result|
27
+ # }
28
+ #
29
+ #
30
+ # We do not include it into gem dependencies as it uses EventMachine
31
+ # which is not needed under JRuby and weight alot (the resto of Farcall plays well with jruby
32
+ # and MRI threads)
33
+ #
34
+ # Due to event-driven nature of eventmachine, WsServerEndpoint uses special version of
35
+ # {EmFarcall::Endpoint} and {EmFarcall::WsProvider} which are code compatible with regular
36
+ # farcall classes except for the callback-style calls where appropriate.
37
+ class WsServerEndpoint < EmFarcall::Endpoint
38
+
39
+ # Create endpoint with the already opened websocket instance. Note that all the handshake
40
+ # should be done prior to construct endpoint (e.g. you may want to have different endpoints
41
+ # for different paths and arguments)
42
+ #
43
+ # See {EmFarcall::Endpoint} for methods to call remote interface and process remote requests.
44
+ #
45
+ # @param [EM::WebSocket] websocket socket in open state (handshake should be passed)
46
+ def initialize websocket, **kwargs
47
+ @input = EM::Channel.new
48
+ @output = EM::Channel.new
49
+ super(@input, @output, **kwargs)
50
+
51
+ websocket.onmessage { |data|
52
+ @input << unpack(data)
53
+ }
54
+ @output.subscribe { |data|
55
+ websocket.send(pack data)
56
+ }
57
+ end
58
+
59
+ def unpack data
60
+ JSON.parse data
61
+ end
62
+
63
+ def pack data
64
+ JSON[data]
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+ rescue LoadError
71
+ $!.to_s =~ /em-websocket/ or raise
72
+ end
73
+
@@ -1,13 +1,19 @@
1
+ require 'hashie'
2
+
1
3
  module Farcall
2
4
 
3
5
  # The protocol endpoint. Takes some transport and implements Farcall protocol over
4
6
  # it. You can use it direcly or with Farcall::RemoteInterface and Farcall::LocalProvider helper
5
7
  # classes.
6
8
  #
9
+ # Note that the returned data is converted to Hashie::Mash primarily for the sake of :key vs.
10
+ # 'key' ambigity that otherwise might appear depending on the transport encoding protocol. Anyway
11
+ # it is better than ruby hash ;)
12
+ #
7
13
  # Endpoint class is thread-safe.
8
14
  class Endpoint
9
15
 
10
- # Set or get provider instance. When provider is set, its methods are called by the remote
16
+ # Set or get provider instance. When provider is set, its public methods are called by the remote
11
17
  # and any possible exception are passed back to caller party. You can use any ruby class instance
12
18
  # everything will work, operators, indexes[] and like.
13
19
  attr_accessor :provider
@@ -59,8 +65,9 @@ module Farcall
59
65
  end
60
66
 
61
67
  # Call remote party. Retruns immediately. When remote party answers, calls the specified block
62
- # if present. The block should take |error, result| parameters. Error could be nil or
63
- # {'class' =>, 'text' => } hash. result is always nil if error is presented.
68
+ # if present. The block should take |error, result| parameters. If result's content hashes
69
+ # or result itself are instances of th Hashie::Mash. Error could be nil or
70
+ # {'class' =>, 'text' => } Hashie::Mash hash. result is always nil if error is presented.
64
71
  #
65
72
  # It is desirable to use Farcall::Endpoint#interface or
66
73
  # Farcall::RemoteInterface rather than this low-level method.
@@ -84,7 +91,8 @@ module Farcall
84
91
  # Farcall::RemoteInterface rather than this low-level method.
85
92
  #
86
93
  # @param [String] name of the remote command
87
- # @return [Object] any data that remote party retruns
94
+ # @return [Object] any data that remote party retruns. If it is a hash, it is a Hashie::Mash
95
+ # instance.
88
96
  # @raise [Farcall::RemoteError]
89
97
  #
90
98
  def sync_call(name, *args, **kwargs)
@@ -143,12 +151,14 @@ module Farcall
143
151
 
144
152
  def _received(data)
145
153
  # p [:r, data]
154
+ data = Hashie::Mash.new data
155
+
146
156
  cmd, serial, args, kwargs, ref, result, error =
147
157
  %w{cmd serial args kwargs ref result error}.map { |k| data[k] || data[k.to_sym] }
148
158
  !serial || serial < 0 and abort 'missing or bad serial'
149
159
 
150
160
  @receive_lock.synchronize {
151
- serial == @in_serial or abort "bad sync"
161
+ serial == @in_serial or abort "framing error (wrong serial)"
152
162
  @in_serial += 1
153
163
  }
154
164
 
@@ -161,15 +171,18 @@ module Farcall
161
171
  if kwargs && !kwargs.empty?
162
172
  # ruby thing: keyqord args must be symbols, not strings:
163
173
  fixed = {}
164
- kwargs.each { |k,v| fixed[k.to_sym] = v}
174
+ kwargs.each { |k, v| fixed[k.to_sym] = v }
165
175
  args << fixed
166
176
  end
167
- @provider.send cmd.to_sym, *args
177
+ @provider.send :remote_call, cmd.to_sym, args
168
178
  elsif @on_remote_call
169
179
  @on_remote_call.call cmd, args, kwargs
170
180
  end
171
181
  _send ref: serial, result: result
172
182
  rescue Exception => e
183
+ # puts e
184
+ # puts e.backtrace.join("\n")
185
+
173
186
  _send ref: serial, error: { 'class' => e.class.name, 'text' => e.to_s }
174
187
  end
175
188
 
@@ -195,23 +208,54 @@ module Farcall
195
208
  # suites you better.
196
209
  #
197
210
  # Please remember that Farcall::Transport instance could be used with only
198
- # one conneced object, unlike Farcall::Endpoint, which could be connected to several
211
+ # one connected object, unlike Farcall::Endpoint, which could be connected to several
199
212
  # consumers.
213
+ #
214
+ # @param [Farcall::Endpoint] endpoint to connect to (no transport should be provided).
215
+ # note that if endpoint is specified, transport would be ignored eeven if used
216
+ # @param [Farcall::Transport] transport to use (don't use endpoint then)
200
217
  def initialize endpoint: nil, transport: nil, **params
201
- @endpoint = if endpoint
202
- endpoint
203
- else
204
- transport ||= Farcall::Transport.create **params
205
- Farcall::Endpoint.new transport
206
- end
207
- @endpoint.provider = self
218
+ if endpoint || transport || params.size > 0
219
+ @endpoint = if endpoint
220
+ endpoint
221
+ else
222
+ transport ||= Farcall::Transport.create **params
223
+ Farcall::Endpoint.new transport
224
+ end
225
+ @endpoint.provider = self
226
+ end
208
227
  end
209
228
 
210
229
  # Get remote interface
211
- # @return [Farcall::Interface] to call methods on the other end
230
+ # @return [Farcall::Interface] to call methods on the other end, e.g. if this provider would
231
+ # like to call other party's methiod, it can do it cimply by:
232
+ #
233
+ # far_interface.some_method('hello')
234
+ #
212
235
  def far_interface
213
236
  @endpoint.remote
214
237
  end
238
+
239
+ # close connection if need
240
+ def close_connection
241
+ @endpoint.close
242
+ end
243
+
244
+ protected
245
+
246
+ # Override it to repond to remote calls. Base implementation let invoke only public method
247
+ # only owned by this class. Be careful to not to expose more than intended!
248
+ def remote_call name, args
249
+ m = public_method(name)
250
+ if m && m.owner == self.class
251
+ m.call(*args)
252
+ else
253
+ raise NoMethodError, "method #{name} is not found"
254
+ end
255
+ rescue NameError
256
+ raise NoMethodError, "method #{name} is not found"
257
+ end
258
+
215
259
  end
216
260
 
217
261
  # Intervace to the remote provider via Farcall protocols. Works the same as if the object
@@ -228,7 +272,7 @@ module Farcall
228
272
  # Create interface connected to some endpoint ar transpost.
229
273
  #
230
274
  # Please remember that Farcall::Transport instance could be used with only
231
- # one conneced object, unlike Farcall::Endpoint, which could be connected to several
275
+ # one connected object, unlike Farcall::Endpoint, which could be connected to several
232
276
  # consumers.
233
277
  #
234
278
  # @param [Farcall::Endpoint|Farcall::Transport] arg either endpoint or a transport
@@ -0,0 +1,74 @@
1
+ require 'thread'
2
+
3
+ # :nodoc:
4
+ class MontorLock
5
+
6
+ def initialize
7
+ @condition = ConditionVariable.new
8
+ @mutex = Mutex.new
9
+ end
10
+
11
+ def notify
12
+ @mutex.synchronize {
13
+ @condition.signal
14
+ }
15
+ end
16
+
17
+ def wait
18
+ @mutex.synchronize {
19
+ @condition.wait(@mutex)
20
+ yield if block_given?
21
+ }
22
+ end
23
+
24
+ end
25
+
26
+ # :nodoc:
27
+ class Semaphore
28
+
29
+ def initialize state_set=false
30
+ @monitor = MontorLock.new
31
+ @state_set = state_set
32
+ end
33
+
34
+ def set?
35
+ @state_set
36
+ end
37
+
38
+ def clear?
39
+ !set?
40
+ end
41
+
42
+ def wait state
43
+ while @state_set != state do
44
+ @monitor.wait
45
+ end
46
+ @state_set
47
+ end
48
+
49
+ def wait_set
50
+ wait true
51
+ end
52
+
53
+ def wait_clear
54
+ wait false
55
+ end
56
+
57
+ def wait_change &block
58
+ @monitor.wait {
59
+ block.call(@state_set) if block
60
+ }
61
+ @state_set
62
+ end
63
+
64
+ def set new_state=true
65
+ if @state_set != new_state
66
+ @state_set = new_state
67
+ @monitor.notify
68
+ end
69
+ end
70
+
71
+ def clear
72
+ set false
73
+ end
74
+ end
@@ -46,7 +46,9 @@ module Farcall
46
46
  end
47
47
 
48
48
  # Tansport must call this process on each incoming hash
49
- # passing it as the only parameter, e.g. self.on_data_received(hash)
49
+ # passing it as the only parameter, e.g. self.on_data_received.call(hash)
50
+ # Common trick is to start inner event loop on on_data_recieved=, don't forget
51
+ # to call super first.
50
52
  attr_accessor :on_data_received, :on_abort, :on_close
51
53
 
52
54
  # Utility function. Calls the provided block on data reception. Resets the
@@ -1,3 +1,3 @@
1
1
  module Farcall
2
- VERSION = "0.1.2"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -0,0 +1,55 @@
1
+ require 'farcall'
2
+ require 'websocket-client-simple'
3
+ require_relative './monitor_lock'
4
+ require 'json'
5
+
6
+ module Farcall
7
+ # Websocket client transport using JSON encodeing. Works with ruby threads, pure ruby, runs
8
+ # everywhere. Use if like any thoer Farcall::Transport, for example:
9
+ #
10
+ # in your Gemfile
11
+ #
12
+ # gem 'websocket-client-simple'
13
+ #
14
+ # in the code
15
+ #
16
+ # wst = Farcall::WebsocketJsonClientTransport.new 'ws://icodici.com:8080/test'
17
+ # i = Farcall::Interface.new transport: wst
18
+ # result = i.authenticate(login, password) # remote call via interface...
19
+ #
20
+ class WebsocketJsonClientTransport < Farcall::Transport
21
+
22
+ # Create transport connected to the specified websocket url. Constructor blocks
23
+ # until connected, or raise error if connection can't be established. Transport uses
24
+ # JSON encodgin over standard websocket protocol.
25
+ def initialize ws_url
26
+ # The stranges bug around in the WebSocket::Client (actually in his eventemitter)
27
+ me = self
28
+
29
+ is_open = Semaphore.new
30
+ @ws = WebSocket::Client::Simple.connect(ws_url)
31
+
32
+ @ws.on(:open) {
33
+ # if me != self
34
+ # puts "\n\n\nSelf is set to wrong in the callback in #{RUBY_VERSION}\n\n\n"
35
+ # end
36
+ # puts "client is open"
37
+ is_open.set
38
+ }
39
+
40
+ @ws.on(:message) { |m|
41
+ # puts "ws client received #{JSON.parse m.data}"
42
+ me.on_data_received and me.on_data_received.call(JSON.parse m.data)
43
+ # puts "and sent"
44
+ }
45
+ @ws.on(:close) { close }
46
+ is_open.wait_set
47
+ end
48
+
49
+ # :nodoc:
50
+ def send_data data
51
+ @ws.send JSON[data]
52
+ end
53
+
54
+ end
55
+ end
data/lib/farcall.rb CHANGED
@@ -8,6 +8,9 @@ begin
8
8
  require 'farcall/boss_transport'
9
9
  rescue LoadError
10
10
  end
11
+ require 'farcall/wsclient_transport'
12
+ require 'farcall/em_wsserver_endpoint'
13
+
11
14
 
12
15
  module Farcall
13
16
  # Your code goes here...
@@ -11,8 +11,35 @@ class TestProvider < Farcall::Provider
11
11
  @a, @b = a, b
12
12
  return "Foo: #{a+b}, #{optional}"
13
13
  end
14
+
15
+ def self.doncallpublic
16
+
17
+ end
18
+
19
+ def get_hash
20
+ { 'foo' => 'bar', 'bardd' => 'buzz', 'last' => 'item', 'bar' => 'test'}
21
+ end
22
+
23
+ private
24
+
25
+ def dontcall
26
+
27
+ end
14
28
  end
15
29
 
30
+ class StringProvider < Farcall::Provider
31
+ def initialize(str)
32
+ @str = str
33
+ end
34
+
35
+ def provide_hash
36
+ { 'bar' => 'test', 'foo' => 'bar'}
37
+ end
38
+
39
+ def value
40
+ @str
41
+ end
42
+ end
16
43
 
17
44
  describe 'endpoint' do
18
45
  include Farcall
@@ -38,14 +65,20 @@ describe 'endpoint' do
38
65
  i.a.should == 5
39
66
  i.b.should == 6
40
67
 
41
- ib.split.should == ['Hello', 'world']
68
+ # ib.split.should == ['Hello', 'world']
69
+
70
+ expect(-> { i.dontcall() }).to raise_error Farcall::RemoteError, /NoMethodError/
71
+ expect(-> { i.sleep() }).to raise_error Farcall::RemoteError, /NoMethodError/
72
+ expect(-> { i.abort() }).to raise_error Farcall::RemoteError, /NoMethodError/
73
+ expect(-> { i.doncallpublic() }).to raise_error Farcall::RemoteError, /NoMethodError/
74
+ expect(-> { i.initialize(1) }).to raise_error Farcall::RemoteError, /NoMethodError/
42
75
  end
43
76
 
44
77
  def check_protocol format
45
78
  s1, s2 = Socket.pair(:UNIX, :STREAM, 0)
46
79
 
47
80
  tp = TestProvider.new socket: s1, format: format
48
- i = Farcall::Interface.new socket: s2, format: format, provider: "Hello world"
81
+ i = Farcall::Interface.new socket: s2, format: format, provider: StringProvider.new("bar")
49
82
 
50
83
  expect(-> { i.foo() }).to raise_error Farcall::RemoteError
51
84
 
@@ -55,14 +88,18 @@ describe 'endpoint' do
55
88
  i.a.should == 5
56
89
  i.b.should == 6
57
90
 
58
- tp.far_interface.split.should == ['Hello', 'world']
91
+ i.get_hash.foo.should == 'bar'
92
+ i.get_hash.bar.should == 'test'
93
+ tp.far_interface.value.should == 'bar'
94
+ tp.far_interface.provide_hash.bar.should == 'test'
95
+ tp.far_interface.provide_hash.foo.should == 'bar'
59
96
  end
60
97
 
61
98
  it 'should connect json via shortcut' do
62
99
  check_protocol :json
63
100
  end
64
101
 
65
- it 'should connect boss via shortcut' do
102
+ it 'boss connect boss via shortcut' do
66
103
  check_protocol :boss
67
104
  end
68
105
 
@@ -0,0 +1,233 @@
1
+ require 'spec_helper'
2
+ require 'eventmachine'
3
+
4
+ def standard_check_wsclient(cnt1, r1, r2, r3)
5
+ r1 = Hashie::Mash.new(r1)
6
+ r2 = Hashie::Mash.new(r2)
7
+
8
+ r1.kwargs.hello.should == 'world'
9
+ r1.superpong.should == [1, 2, 3]
10
+ r2.kwargs.hello.should == 'world'
11
+ r2.superpong.should == [1, 2, 10]
12
+
13
+ cnt1.should == 2
14
+ end
15
+
16
+ def standard_check(cnt1, r1, r2, r3)
17
+ r1.error.should == nil
18
+ r1.result.kwargs.hello.should == 'world'
19
+ r1.result.superpong.should == [1, 2, 3]
20
+ r2.error.should == nil
21
+ r2.result.kwargs.hello.should == 'world'
22
+ r2.result.superpong.should == [1, 2, 10]
23
+ r3.error[:class].should == 'RuntimeError'
24
+ r3.error.text.should == 'test error'
25
+ cnt1.should == 3
26
+ end
27
+
28
+
29
+ def setup_endpoints
30
+ c1, c2, c3, c4 = 4.times.map { EM::Channel.new }
31
+
32
+ @e1 = EmFarcall::Endpoint.new c1, c2
33
+ @e2 = EmFarcall::Endpoint.new c3, c4
34
+
35
+ c2.subscribe { |x| c3 << x }
36
+ c4.subscribe { |x| c1 << x }
37
+
38
+ @e2.on :superping do |*args, **kwargs|
39
+ if kwargs[:need_error]
40
+ raise 'test error'
41
+ end
42
+ { superpong: args, kwargs: kwargs }
43
+ end
44
+ [@e1, @e2]
45
+ end
46
+
47
+ describe 'em_farcall' do
48
+
49
+ it 'exchange messages' do
50
+ r1 = nil
51
+ r2 = nil
52
+ r3 = nil
53
+ cnt1 = 0
54
+
55
+ EM.run {
56
+ e1, e2 = setup_endpoints
57
+ e1.call 'superping', 1, 2, 3, hello: 'world' do |r|
58
+ r1 = r
59
+ cnt1 += 1
60
+ end
61
+
62
+ e1.call 'superping', 1, 2, 10, hello: 'world' do |r|
63
+ r2 = r
64
+ cnt1 += 1
65
+ end
66
+
67
+ e1.call 'superping', 1, 2, 10, need_error: true, hello: 'world' do |r|
68
+ r3 = r
69
+ cnt1 += 1
70
+ EM.stop
71
+ end
72
+
73
+ EM.add_timer(4) {
74
+ EM.stop
75
+ }
76
+ }
77
+ standard_check(cnt1, r1, r2, r3)
78
+ end
79
+
80
+ it 'uses remote interface' do
81
+ r1 = nil
82
+ r2 = nil
83
+ r3 = nil
84
+ cnt1 = 0
85
+
86
+ EM.run {
87
+ e1, e2 = setup_endpoints
88
+ i = EmFarcall::Interface.new e1
89
+
90
+ i.superping(1, 2, 3, hello: 'world') { |r|
91
+ r1 = r
92
+ cnt1 += 1
93
+ }
94
+
95
+ i.superping 1, 2, 10, hello: 'world' do |r|
96
+ r2 = r
97
+ cnt1 += 1
98
+ end
99
+
100
+ i.superping 1, 2, 10, need_error: true, hello: 'world' do |r|
101
+ r3 = r
102
+ cnt1 += 1
103
+ EM.stop
104
+ end
105
+
106
+ i.test_not_existing
107
+
108
+ EM.add_timer(4) {
109
+ EM.stop
110
+ }
111
+ }
112
+ standard_check(cnt1, r1, r2, r3)
113
+ end
114
+
115
+ it 'runs via websockets' do
116
+ r1 = nil
117
+ r2 = nil
118
+ r3 = nil
119
+ cnt1 = 0
120
+
121
+ EM.run {
122
+ params = {
123
+ :host => 'localhost',
124
+ :port => 8088
125
+ }
126
+ e1 = nil
127
+ e2 = nil
128
+
129
+ EM::WebSocket.run(params) do |ws|
130
+ ws.onopen { |handshake|
131
+ e2 = EmFarcall::WsServerEndpoint.new ws
132
+
133
+ e2.on :superping do |*args, **kwargs|
134
+ if kwargs[:need_error]
135
+ raise 'test error'
136
+ end
137
+ { superpong: args, kwargs: kwargs }
138
+ end
139
+ }
140
+ end
141
+
142
+
143
+ EM.defer {
144
+ t1 = Farcall::WebsocketJsonClientTransport.new 'ws://localhost:8088/test'
145
+ i = Farcall::Interface.new transport: t1
146
+
147
+ r1 = i.superping(1, 2, 3, hello: 'world')
148
+ cnt1 += 1
149
+
150
+ r2 = i.superping 1, 2, 10, hello: 'world'
151
+ cnt1 += 1
152
+
153
+ expect {
154
+ r3 = i.superping 1, 2, 10, need_error: true, hello: 'world'
155
+ }.to raise_error(Farcall::RemoteError, "RuntimeError: test error")
156
+
157
+ expect {
158
+ r3 = i.superping_bad 13, 2, 10, hello: 'world'
159
+ }.to raise_error(Farcall::RemoteError, /NoMethodError/)
160
+
161
+
162
+ EM.stop
163
+ }
164
+ EM.add_timer(4) {
165
+ EM.stop
166
+ }
167
+ }
168
+
169
+ standard_check_wsclient(cnt1, r1, r2, r3)
170
+ end
171
+
172
+
173
+ class WsProvider < EmFarcall::Provider
174
+ def superping *args, **kwargs
175
+ if kwargs[:need_error]
176
+ raise 'test error'
177
+ end
178
+ { superpong: args, kwargs: kwargs }
179
+ end
180
+ end
181
+
182
+ it 'runs via websockets with provider' do
183
+ r1 = nil
184
+ r2 = nil
185
+ r3 = nil
186
+ cnt1 = 0
187
+
188
+ EM.run {
189
+ params = {
190
+ :host => 'localhost',
191
+ :port => 8088
192
+ }
193
+ e1 = nil
194
+ e2 = nil
195
+
196
+ EM::WebSocket.run(params) do |ws|
197
+ ws.onopen { |handshake|
198
+ e2 = EmFarcall::WsServerEndpoint.new ws, provider: WsProvider.new
199
+ }
200
+ end
201
+
202
+
203
+ EM.defer {
204
+ t1 = Farcall::WebsocketJsonClientTransport.new 'ws://localhost:8088/test'
205
+ i = Farcall::Interface.new transport: t1
206
+
207
+ r1 = i.superping(1, 2, 3, hello: 'world')
208
+ cnt1 += 1
209
+
210
+ r2 = i.superping 1, 2, 10, hello: 'world'
211
+ cnt1 += 1
212
+
213
+ expect {
214
+ r3 = i.superping 1, 2, 11, need_error: true, hello: 'world'
215
+ }.to raise_error(Farcall::RemoteError, "RuntimeError: test error")
216
+
217
+ expect {
218
+ r3 = i.superping_bad 13, 2, 10, need_error: true, hello: 'world'
219
+ }.to raise_error(Farcall::RemoteError, /NoMethodError/)
220
+
221
+
222
+ EM.stop
223
+ }
224
+ EM.add_timer(4) {
225
+ EM.stop
226
+ }
227
+ }
228
+
229
+ standard_check_wsclient(cnt1, r1, r2, r3)
230
+ end
231
+
232
+
233
+ end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: farcall
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - sergeych
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-28 00:00:00.000000000 Z
11
+ date: 2016-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: hashie
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,20 +66,48 @@ dependencies:
52
66
  - - ">="
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: em-websocket
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: websocket-client-simple
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
55
97
  - !ruby/object:Gem::Dependency
56
98
  name: boss-protocol
57
99
  requirement: !ruby/object:Gem::Requirement
58
100
  requirements:
59
101
  - - ">="
60
102
  - !ruby/object:Gem::Version
61
- version: 1.4.1
103
+ version: 1.4.3
62
104
  type: :development
63
105
  prerelease: false
64
106
  version_requirements: !ruby/object:Gem::Requirement
65
107
  requirements:
66
108
  - - ">="
67
109
  - !ruby/object:Gem::Version
68
- version: 1.4.1
110
+ version: 1.4.3
69
111
  description: |2
70
112
  Simple and effective cross-platform RPC protocol. Can work with any transport capable to
71
113
  pass structures (dictionaries, hashes, whatever you name it). Out of the box provides
@@ -85,15 +127,19 @@ files:
85
127
  - farcall.gemspec
86
128
  - lib/farcall.rb
87
129
  - lib/farcall/boss_transport.rb
130
+ - lib/farcall/em_farcall.rb
131
+ - lib/farcall/em_wsserver_endpoint.rb
88
132
  - lib/farcall/endpoint.rb
89
133
  - lib/farcall/json_transport.rb
134
+ - lib/farcall/monitor_lock.rb
90
135
  - lib/farcall/transport.rb
91
136
  - lib/farcall/version.rb
137
+ - lib/farcall/wsclient_transport.rb
92
138
  - spec/endpoint_spec.rb
93
- - spec/rmi_spec.rb
94
139
  - spec/spec_helper.rb
95
140
  - spec/transports_spec.rb
96
- homepage: ''
141
+ - spec/websock_spec.rb
142
+ homepage: https://github.com/sergeych/farcall
97
143
  licenses:
98
144
  - MIT
99
145
  metadata: {}
@@ -113,12 +159,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
159
  version: '0'
114
160
  requirements: []
115
161
  rubyforge_project:
116
- rubygems_version: 2.4.5
162
+ rubygems_version: 2.5.1
117
163
  signing_key:
118
164
  specification_version: 4
119
165
  summary: Simple, elegant and cross-platofrm RPC protocol
120
166
  test_files:
121
167
  - spec/endpoint_spec.rb
122
- - spec/rmi_spec.rb
123
168
  - spec/spec_helper.rb
124
169
  - spec/transports_spec.rb
170
+ - spec/websock_spec.rb
data/spec/rmi_spec.rb DELETED
@@ -1,7 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe 'rmi' do
4
-
5
- it 'should connect existing objects and'
6
-
7
- end