farcall 0.0.1 → 0.1.0
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.
- checksums.yaml +4 -4
- data/.rspec +2 -0
- data/README.md +55 -3
- data/farcall.gemspec +1 -0
- data/lib/farcall.rb +4 -1
- data/lib/farcall/endpoint.rb +258 -0
- data/lib/farcall/json_transport.rb +141 -0
- data/lib/farcall/transport.rb +112 -0
- data/lib/farcall/version.rb +1 -1
- data/spec/endpoint_spec.rb +62 -0
- data/spec/spec_helper.rb +100 -0
- data/spec/transports_spec.rb +74 -0
- metadata +26 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28f543ad602b3ae18360d4e445c79c60f9999e89
|
4
|
+
data.tar.gz: 5c560d5137bba206c11a216e3005bb5ec7f2713c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9696bda5293c960cb672aeb14a5d47a640d4db7673a59f1a4aa2a2bded5d9fd80cf4f1d302f83a9c404bef236b480ebb8bc52250519f86cf00d80319d17f8273
|
7
|
+
data.tar.gz: 07128d134976c41240dd64612b575f475ef5d6e2884657729cfa1a81f89f50ffaf8aba8debd87589140e449e9a682033bacc89496c80f8fc85aa433a6e8bc8e1
|
data/.rspec
ADDED
data/README.md
CHANGED
@@ -1,9 +1,61 @@
|
|
1
1
|
# Farcall
|
2
2
|
|
3
|
-
|
3
|
+
## Important!
|
4
4
|
|
5
|
-
The gem creation is under
|
5
|
+
The gem creation is under active development, current state is: beta. Only JSON format is supported.
|
6
6
|
|
7
|
+
## Description
|
8
|
+
|
9
|
+
The simple and elegant cross-platform RPC protocol that uses any formatter/transport capable of
|
10
|
+
transmitting dictionary-like objects, for example, JSON, XML, BSON, BOSS and many others.
|
11
|
+
|
12
|
+
RPC is made asynchronously, each call can have any return values. While one call is waiting,
|
13
|
+
other calls can be executed. The protocol is bidirectional Call parameters could be
|
14
|
+
both arrays of arguments and keyword arguments, return value could be any object, e.g. array,
|
15
|
+
dictionary, wahtever.
|
16
|
+
|
17
|
+
Exception/errors transmitting is also supported.
|
18
|
+
|
19
|
+
The interface is very simple and rubyish:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
# The sample class that exports all its methods to the remote callers:
|
23
|
+
#
|
24
|
+
class TestProvider < Farcall::Provider
|
25
|
+
|
26
|
+
attr :foo_calls, :a, :b
|
27
|
+
|
28
|
+
def foo a, b, optional: 'none'
|
29
|
+
@foo_calls = (@foo_calls || 0) + 1
|
30
|
+
@a, @b = a, b
|
31
|
+
return "Foo: #{a+b}, #{optional}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# create instance and export it to some connected socket:
|
36
|
+
|
37
|
+
TestProvider.new socket: connected_socket # default format is JSON
|
38
|
+
```
|
39
|
+
|
40
|
+
Suppose whe have some socket connected to one above, then TestProvider methods are available via
|
41
|
+
this connection:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
i = Farcall::Interface.new socket: client_socket
|
45
|
+
|
46
|
+
# Plain arguments
|
47
|
+
i.foo(10, 20).should == 'Foo: 30, none'
|
48
|
+
|
49
|
+
# Plain and keyword arguments
|
50
|
+
i.foo(5, 6, optional: 'yes!').should == 'Foo: 11, yes!'
|
51
|
+
|
52
|
+
# the exceptions on the remote side are conveyed:
|
53
|
+
expect(-> { i.foo() }).to raise_error Farcall::RemoteError
|
54
|
+
|
55
|
+
# new we can read results from the remote side state:
|
56
|
+
i.a.should == 5
|
57
|
+
i.b.should == 6
|
58
|
+
```
|
7
59
|
|
8
60
|
## Installation
|
9
61
|
|
@@ -23,7 +75,7 @@ Or install it yourself as:
|
|
23
75
|
|
24
76
|
## Usage
|
25
77
|
|
26
|
-
|
78
|
+
Try to get autodocs. Sorry for now.
|
27
79
|
|
28
80
|
## Contributing
|
29
81
|
|
data/farcall.gemspec
CHANGED
data/lib/farcall.rb
CHANGED
@@ -0,0 +1,258 @@
|
|
1
|
+
module Farcall
|
2
|
+
|
3
|
+
# The protocol endpoint. Takes some transport and implements Farcall protocol over
|
4
|
+
# it. You can use it direcly or with Farcall::RemoteInterface and Farcall::LocalProvider helper
|
5
|
+
# classes.
|
6
|
+
#
|
7
|
+
# Endpoint class is thread-safe.
|
8
|
+
class Endpoint
|
9
|
+
|
10
|
+
# Set or get provider instance. When provider is set, its methods are called by the remote
|
11
|
+
# and any possible exception are passed back to caller party. You can use any ruby class instance
|
12
|
+
# everything will work, operators, indexes[] and like.
|
13
|
+
attr_accessor :provider
|
14
|
+
|
15
|
+
# Create endpoint connected to some transport
|
16
|
+
# @param [Farcall::Transport] transport
|
17
|
+
def initialize(transport)
|
18
|
+
@transport = transport
|
19
|
+
@in_serial = @out_serial = 0
|
20
|
+
@transport.on_data_received = -> (data) {
|
21
|
+
begin
|
22
|
+
_received(data)
|
23
|
+
rescue
|
24
|
+
abort :format_error, $!
|
25
|
+
end
|
26
|
+
}
|
27
|
+
@send_lock = Mutex.new
|
28
|
+
@receive_lock = Mutex.new
|
29
|
+
|
30
|
+
@waiting = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
# The provided block will be called if endpoint functioning will be aborted.
|
34
|
+
# The block should take |reason, exception| parameters - latter could be nil
|
35
|
+
def on_abort &proc
|
36
|
+
@abort_hadnler = proc
|
37
|
+
end
|
38
|
+
|
39
|
+
# Add the close handler. Specified block will be called when the endpoint is been closed
|
40
|
+
def on_close &block
|
41
|
+
@close_handler = block
|
42
|
+
end
|
43
|
+
|
44
|
+
# :nodoc:
|
45
|
+
def abort reason, exception = nil
|
46
|
+
puts "*** Abort: reason #{reason || exception.to_s}"
|
47
|
+
@abort_hadnler and @abort_hadnler.call reason, exception
|
48
|
+
if exception
|
49
|
+
raise exception
|
50
|
+
end
|
51
|
+
close
|
52
|
+
end
|
53
|
+
|
54
|
+
# Close endpoint and connected transport
|
55
|
+
def close
|
56
|
+
@transport.close
|
57
|
+
@transport = nil
|
58
|
+
@close_handler and @close_handler.call
|
59
|
+
end
|
60
|
+
|
61
|
+
# 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.
|
64
|
+
#
|
65
|
+
# It is desirable to use Farcall::Endpoint#interface or
|
66
|
+
# Farcall::RemoteInterface rather than this low-level method.
|
67
|
+
#
|
68
|
+
# @param [String] name of the remote command
|
69
|
+
def call(name, *args, **kwargs, &block)
|
70
|
+
@send_lock.synchronize {
|
71
|
+
if block != nil
|
72
|
+
@waiting[@out_serial] = {
|
73
|
+
time: Time.new,
|
74
|
+
proc: block
|
75
|
+
}
|
76
|
+
_send(cmd: name.to_s, args: args, kwargs: kwargs)
|
77
|
+
end
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
# Call the remote party and wait for the return.
|
82
|
+
#
|
83
|
+
# It is desirable to use Farcall::Endpoint#interface or
|
84
|
+
# Farcall::RemoteInterface rather than this low-level method.
|
85
|
+
#
|
86
|
+
# @param [String] name of the remote command
|
87
|
+
# @return [Object] any data that remote party retruns
|
88
|
+
# @raise [Farcall::RemoteError]
|
89
|
+
#
|
90
|
+
def sync_call(name, *args, **kwargs)
|
91
|
+
mutex = Mutex.new
|
92
|
+
resource = ConditionVariable.new
|
93
|
+
error = nil
|
94
|
+
result = nil
|
95
|
+
calling_thread = Thread.current
|
96
|
+
|
97
|
+
mutex.synchronize {
|
98
|
+
same_thread = false
|
99
|
+
call(name, *args, **kwargs) { |e, r|
|
100
|
+
error, result = e, r
|
101
|
+
# Absolutly stupid wait for self situation
|
102
|
+
# When single thread is used to send and receive
|
103
|
+
# - often happens in test environments
|
104
|
+
if calling_thread == Thread.current
|
105
|
+
same_thread = true
|
106
|
+
else
|
107
|
+
resource.signal
|
108
|
+
end
|
109
|
+
}
|
110
|
+
same_thread or resource.wait(mutex)
|
111
|
+
}
|
112
|
+
if error
|
113
|
+
raise Farcall::RemoteError.new(error['class'], error['text'])
|
114
|
+
end
|
115
|
+
result
|
116
|
+
end
|
117
|
+
|
118
|
+
# Process remote commands. Not that provider have precedence at the moment.
|
119
|
+
# Provided block will be executed on every remote command taking parameters
|
120
|
+
# |name, args, kwargs|. Whatever block returns will be passed to a calling party.
|
121
|
+
# The same any exception that the block might raise would be send back to caller.
|
122
|
+
def on_remote_call &block
|
123
|
+
@on_remote_call = block
|
124
|
+
end
|
125
|
+
|
126
|
+
# Get the Farcall::RemoteInterface connnected to this endpoint. Any subsequent calls with
|
127
|
+
# return the same instance.
|
128
|
+
def remote
|
129
|
+
@remote ||= Farcall::Interface.new endpoint: self
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def _send(**kwargs)
|
135
|
+
if @send_lock.locked?
|
136
|
+
kwargs[:serial] = @out_serial
|
137
|
+
@transport.send_data kwargs
|
138
|
+
@out_serial += 1
|
139
|
+
else
|
140
|
+
@send_lock.synchronize { _send(**kwargs) }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def _received(data)
|
145
|
+
# p [:r, data]
|
146
|
+
cmd, serial, args, kwargs, ref, result, error =
|
147
|
+
%w{cmd serial args kwargs ref result error}.map { |k| data[k] || data[k.to_sym] }
|
148
|
+
!serial || serial < 0 and abort 'missing or bad serial'
|
149
|
+
|
150
|
+
@receive_lock.synchronize {
|
151
|
+
serial == @in_serial or abort "bad sync"
|
152
|
+
@in_serial += 1
|
153
|
+
}
|
154
|
+
|
155
|
+
case
|
156
|
+
when cmd
|
157
|
+
|
158
|
+
begin
|
159
|
+
result = if @provider
|
160
|
+
args ||= []
|
161
|
+
if kwargs && !kwargs.empty?
|
162
|
+
# ruby thing: keyqord args must be symbols, not strings:
|
163
|
+
fixed = {}
|
164
|
+
kwargs.each { |k,v| fixed[k.to_sym] = v}
|
165
|
+
args << fixed
|
166
|
+
end
|
167
|
+
@provider.send cmd.to_sym, *args
|
168
|
+
elsif @on_remote_call
|
169
|
+
@on_remote_call.call cmd, args, kwargs
|
170
|
+
end
|
171
|
+
_send ref: serial, result: result
|
172
|
+
rescue Exception => e
|
173
|
+
_send ref: serial, error: { 'class' => e.class.name, 'text' => e.to_s }
|
174
|
+
end
|
175
|
+
|
176
|
+
when ref
|
177
|
+
|
178
|
+
ref or abort 'no reference in return'
|
179
|
+
(w = @waiting.delete ref) != nil and w[:proc].call(error, result)
|
180
|
+
|
181
|
+
else
|
182
|
+
abort 'unknown command'
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
# Could be used as a base class to export its methods to the remote. You are not limited
|
189
|
+
# to subclassing, instead, you can set any class instance as a provider setting it to
|
190
|
+
# the Farcall::Endpoint#provider. The provider has only one method^ which can not be accessed
|
191
|
+
# remotely: #far_interface, which is used locally to object interface to call remote methods
|
192
|
+
# for two-way connections.
|
193
|
+
class Provider
|
194
|
+
# Create an instance connected to the Farcall::Transport or Farcall::Endpoint - use what
|
195
|
+
# suites you better.
|
196
|
+
#
|
197
|
+
# Please remember that Farcall::Transport instance could be used with only
|
198
|
+
# one conneced object, unlike Farcall::Endpoint, which could be connected to several
|
199
|
+
# consumers.
|
200
|
+
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
|
208
|
+
end
|
209
|
+
|
210
|
+
# Get remote interface
|
211
|
+
# @return [Farcall::Interface] to call methods on the other end
|
212
|
+
def far_interface
|
213
|
+
@endpoint.remote
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Intervace to the remote provider via Farcall protocols. Works the same as if the object
|
218
|
+
# would be in local data, but slower :) The same as calling Farcall::Endpoint#interface
|
219
|
+
#
|
220
|
+
# RemoteInterface transparently creates methods as you call them to speedup subsequent
|
221
|
+
# calls.
|
222
|
+
#
|
223
|
+
# There is no way to check that the remote responds to some method other than call it and
|
224
|
+
# catch the exception
|
225
|
+
#
|
226
|
+
class Interface
|
227
|
+
|
228
|
+
# Create interface connected to some endpoint ar transpost.
|
229
|
+
#
|
230
|
+
# Please remember that Farcall::Transport instance could be used with only
|
231
|
+
# one conneced object, unlike Farcall::Endpoint, which could be connected to several
|
232
|
+
# consumers.
|
233
|
+
#
|
234
|
+
# @param [Farcall::Endpoint|Farcall::Transport] arg either endpoint or a transport
|
235
|
+
# to connect interface to
|
236
|
+
def initialize endpoint: nil, transport: nil, provider: nil, **params
|
237
|
+
@endpoint = if endpoint
|
238
|
+
endpoint
|
239
|
+
else
|
240
|
+
Farcall::Endpoint.new(transport || Farcall::Transport.create(**params))
|
241
|
+
end
|
242
|
+
provider and @endpoint.provider = provider
|
243
|
+
end
|
244
|
+
|
245
|
+
def method_missing(method_name, *arguments, **kw_arguments, &block)
|
246
|
+
instance_eval <<-End
|
247
|
+
def #{method_name} *arguments, **kw_arguments
|
248
|
+
@endpoint.sync_call '#{method_name}', *arguments, **kw_arguments
|
249
|
+
end
|
250
|
+
End
|
251
|
+
@endpoint.sync_call method_name, *arguments, **kw_arguments
|
252
|
+
end
|
253
|
+
|
254
|
+
def respond_to_missing?(method_name, include_private = false)
|
255
|
+
true
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Farcall
|
4
|
+
|
5
|
+
# Stream-like object to wrap very strange ruby socket IO
|
6
|
+
class SocketStream
|
7
|
+
|
8
|
+
def initialize socket
|
9
|
+
@socket = socket
|
10
|
+
end
|
11
|
+
|
12
|
+
def read length=1
|
13
|
+
data = ''
|
14
|
+
while data.length < length
|
15
|
+
data << @socket.recv(length - data.length, Socket::MSG_WAITALL)
|
16
|
+
end
|
17
|
+
data
|
18
|
+
end
|
19
|
+
|
20
|
+
def write data
|
21
|
+
@socket.write data
|
22
|
+
end
|
23
|
+
|
24
|
+
def << data
|
25
|
+
write data
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
# The socket stream that imitates slow data reception over the slow internet connection
|
31
|
+
# use to for testing only
|
32
|
+
class DebugSocketStream < Farcall::SocketStream
|
33
|
+
|
34
|
+
# @param [float] timeout between sending individual bytes in seconds
|
35
|
+
def initialize socket, timeout
|
36
|
+
super socket
|
37
|
+
@timeout = timeout
|
38
|
+
end
|
39
|
+
|
40
|
+
def write data
|
41
|
+
data.to_s.each_char { |x|
|
42
|
+
super x
|
43
|
+
sleep @timeout
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# :nodoc:
|
49
|
+
module TransportBase
|
50
|
+
# connect socket or use streams if any
|
51
|
+
def setup_streams input: nil, output: nil, socket: nil
|
52
|
+
if socket
|
53
|
+
@socket = socket
|
54
|
+
@input = @output = SocketStream.new(socket)
|
55
|
+
else
|
56
|
+
@input, @output = input, output
|
57
|
+
end
|
58
|
+
@input != nil && @output != nil or raise Farcall::Error, "can't setup streams"
|
59
|
+
end
|
60
|
+
|
61
|
+
# close connection (socket or streams)
|
62
|
+
def close_connection
|
63
|
+
if @socket
|
64
|
+
if !@socket.closed?
|
65
|
+
begin
|
66
|
+
@socket.flush
|
67
|
+
@socket.shutdown
|
68
|
+
rescue Errno::ENOTCONN
|
69
|
+
end
|
70
|
+
@socket.close
|
71
|
+
end
|
72
|
+
@socket = nil
|
73
|
+
else
|
74
|
+
@input.close
|
75
|
+
@output.close
|
76
|
+
end
|
77
|
+
@input = @output = nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# The transport that uses delimited texts formatted with JSON. Delimiter should be a character
|
82
|
+
# sequence that will never appear in data, by default "\x00" is used. Also several \n\n\n can be
|
83
|
+
# used, most JSON codecs never insert several empty strings
|
84
|
+
class JsonTransport < Farcall::Transport
|
85
|
+
include TransportBase
|
86
|
+
|
87
|
+
# Create json transport, see Farcall::Transpor#create for parameters
|
88
|
+
def initialize delimiter: "\x00", **params
|
89
|
+
setup_streams **params
|
90
|
+
@delimiter = delimiter
|
91
|
+
@dlength = -delimiter.length
|
92
|
+
end
|
93
|
+
|
94
|
+
def on_data_received= block
|
95
|
+
super
|
96
|
+
if block && !@thread
|
97
|
+
@thread = Thread.start {
|
98
|
+
load_loop
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def send_data hash
|
104
|
+
@output << JSON.unparse(hash) + @delimiter
|
105
|
+
end
|
106
|
+
|
107
|
+
def close
|
108
|
+
if !@closing
|
109
|
+
@closing = true
|
110
|
+
close_connection
|
111
|
+
@thread and @thread.join
|
112
|
+
@thread = nil
|
113
|
+
@in_close = false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def load_loop
|
120
|
+
buffer = ''
|
121
|
+
while true
|
122
|
+
buffer << @input.read(1)
|
123
|
+
if buffer[@dlength..-1] == @delimiter
|
124
|
+
on_data_received and on_data_received.call(JSON.parse(buffer[0...@dlength]))
|
125
|
+
buffer = ''
|
126
|
+
end
|
127
|
+
end
|
128
|
+
rescue Errno::EPIPE
|
129
|
+
close
|
130
|
+
rescue
|
131
|
+
if !@closing
|
132
|
+
STDERR.puts "Farcall::JsonTransport read loop failed: #{$!.class.name}: #$!"
|
133
|
+
connection_aborted $!
|
134
|
+
else
|
135
|
+
close
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
@@ -0,0 +1,112 @@
|
|
1
|
+
|
2
|
+
module Farcall
|
3
|
+
|
4
|
+
# Generic error in Farcall library
|
5
|
+
class Error < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# The error occured while executin remote method
|
9
|
+
class RemoteError < Error
|
10
|
+
attr :remote_class
|
11
|
+
|
12
|
+
def initialize remote_class, text
|
13
|
+
@remote_class = remote_class
|
14
|
+
super "#{remote_class}: #{text}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# The transport interface. Farcall works via anything that can send and receive dictionary
|
19
|
+
# objects. The transport should only implement Transport#send_data and invoke
|
20
|
+
# Transport#on_data_received when incoming data are available
|
21
|
+
class Transport
|
22
|
+
|
23
|
+
# Create transport with a given format and parameters.
|
24
|
+
#
|
25
|
+
# format right now can be only :json
|
26
|
+
#
|
27
|
+
# creation parameters can be:
|
28
|
+
#
|
29
|
+
# - socket: connect transport to some socket (should be connected)
|
30
|
+
#
|
31
|
+
# - input and aoutput: two stream-like objects which support read(length) and write(data)
|
32
|
+
# parameters
|
33
|
+
#
|
34
|
+
def self.create format: :json, **params
|
35
|
+
case format
|
36
|
+
when :json
|
37
|
+
Farcall::JsonTransport.new **params
|
38
|
+
else
|
39
|
+
raise Farcall::Error, "unknown format: #{format}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
attr :closed
|
44
|
+
|
45
|
+
# Tansport must call this process on each incoming hash
|
46
|
+
# passing it as the only parameter, e.g. self.on_data_received(hash)
|
47
|
+
attr_accessor :on_data_received, :on_abort, :on_close
|
48
|
+
|
49
|
+
# Utility function. Calls the provided block on data reception. Resets the
|
50
|
+
# block with #on_data_received
|
51
|
+
def receive_data &block
|
52
|
+
self.on_data_received = block
|
53
|
+
end
|
54
|
+
|
55
|
+
# Transmit somehow a dictionary to the remote part
|
56
|
+
def send_data hash
|
57
|
+
raise 'not implemented'
|
58
|
+
end
|
59
|
+
|
60
|
+
# Flush and close transport
|
61
|
+
def close
|
62
|
+
@closed = true
|
63
|
+
@on_close and @on_close.call
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def connection_closed
|
69
|
+
close
|
70
|
+
end
|
71
|
+
|
72
|
+
def connection_aborted exceptoin
|
73
|
+
STDERR.puts "Farcall: connection aborted: #{$!.class.name}: #{$!}"
|
74
|
+
@on_abort and @on_abort.call $!
|
75
|
+
close
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
# Test connection that provides 2 interconnected transports
|
82
|
+
# TestConnection#a and TestConnection#b that could be used to connect Endpoints
|
83
|
+
class LocalConnection
|
84
|
+
|
85
|
+
# :nodoc:
|
86
|
+
class Connection < Transport
|
87
|
+
|
88
|
+
attr_accessor :other
|
89
|
+
|
90
|
+
def initialize other_loop = nil
|
91
|
+
if other_loop
|
92
|
+
other_loop.other = self
|
93
|
+
@other = other_loop
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def send_data hash
|
98
|
+
@other.on_data_received.call hash
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
attr :a, :b
|
104
|
+
|
105
|
+
def initialize
|
106
|
+
@a = Connection.new
|
107
|
+
@b = Connection.new @a
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
end
|
data/lib/farcall/version.rb
CHANGED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
# The sample class that exports all its methods to the remote callers:
|
5
|
+
#
|
6
|
+
class TestProvider < Farcall::Provider
|
7
|
+
|
8
|
+
attr :foo_calls, :a, :b
|
9
|
+
|
10
|
+
def foo a, b, optional: 'none'
|
11
|
+
@foo_calls = (@foo_calls || 0) + 1
|
12
|
+
@a, @b = a, b
|
13
|
+
return "Foo: #{a+b}, #{optional}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'endpoint' do
|
18
|
+
include Farcall
|
19
|
+
|
20
|
+
it 'should do RPC call with provider/interface' do
|
21
|
+
tc = Farcall::LocalConnection.new
|
22
|
+
|
23
|
+
ea = Farcall::Endpoint.new tc.a
|
24
|
+
eb = Farcall::Endpoint.new tc.b
|
25
|
+
|
26
|
+
TestProvider.new endpoint: ea
|
27
|
+
eb.provider = "Hello world"
|
28
|
+
|
29
|
+
i = Farcall::Interface.new endpoint: eb
|
30
|
+
i2 = Farcall::Interface.new endpoint: eb
|
31
|
+
ib = Farcall::Interface.new endpoint: ea
|
32
|
+
|
33
|
+
expect(-> { i.foo() }).to raise_error Farcall::RemoteError
|
34
|
+
|
35
|
+
i.foo(10, 20).should == 'Foo: 30, none'
|
36
|
+
i2.foo(5, 6, optional: 'yes!').should == 'Foo: 11, yes!'
|
37
|
+
|
38
|
+
i.a.should == 5
|
39
|
+
i.b.should == 6
|
40
|
+
|
41
|
+
ib.split.should == ['Hello', 'world']
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should connect via shortcut' do
|
45
|
+
s1, s2 = Socket.pair(:UNIX, :STREAM, 0)
|
46
|
+
|
47
|
+
tp = TestProvider.new socket: s1, format: :json
|
48
|
+
i = Farcall::Interface.new socket: s2, format: :json, provider: "Hello world"
|
49
|
+
|
50
|
+
|
51
|
+
expect(-> { i.foo() }).to raise_error Farcall::RemoteError
|
52
|
+
|
53
|
+
i.foo(10, 20).should == 'Foo: 30, none'
|
54
|
+
i.foo(5, 6, optional: 'yes!').should == 'Foo: 11, yes!'
|
55
|
+
|
56
|
+
i.a.should == 5
|
57
|
+
i.b.should == 6
|
58
|
+
|
59
|
+
tp.far_interface.split.should == ['Hello', 'world']
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'farcall'
|
2
|
+
|
3
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
6
|
+
# this file to always be loaded, without a need to explicitly require it in any
|
7
|
+
# files.
|
8
|
+
#
|
9
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
10
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
11
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
12
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
13
|
+
# a separate helper file that requires the additional dependencies and performs
|
14
|
+
# the additional setup, and require it from the spec files that actually need
|
15
|
+
# it.
|
16
|
+
#
|
17
|
+
# The `.rspec` file also contains a few flags that are not defaults but that
|
18
|
+
# users commonly want.
|
19
|
+
#
|
20
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
21
|
+
RSpec.configure do |config|
|
22
|
+
# rspec-expectations config goes here. You can use an alternate
|
23
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
24
|
+
# assertions if you prefer.
|
25
|
+
config.expect_with :rspec do |expectations|
|
26
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
27
|
+
# and `failure_message` of custom matchers include text for helper methods
|
28
|
+
# defined using `chain`, e.g.:
|
29
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
30
|
+
# # => "be bigger than 2 and smaller than 4"
|
31
|
+
# ...rather than:
|
32
|
+
# # => "be bigger than 2"
|
33
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
34
|
+
expectations.syntax = [:should, :expect]
|
35
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
36
|
+
end
|
37
|
+
|
38
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
39
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
40
|
+
config.mock_with :rspec do |mocks|
|
41
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
42
|
+
# a real object. This is generally recommended, and will default to
|
43
|
+
# `true` in RSpec 4.
|
44
|
+
mocks.verify_partial_doubles = true
|
45
|
+
end
|
46
|
+
|
47
|
+
# The settings below are suggested to provide a good initial experience
|
48
|
+
# with RSpec, but feel free to customize to your heart's content.
|
49
|
+
=begin
|
50
|
+
# These two settings work together to allow you to limit a spec run
|
51
|
+
# to individual examples or groups you care about by tagging them with
|
52
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
53
|
+
# get run.
|
54
|
+
config.filter_run :focus
|
55
|
+
config.run_all_when_everything_filtered = true
|
56
|
+
|
57
|
+
# Allows RSpec to persist some state between runs in order to support
|
58
|
+
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
59
|
+
# you configure your source control system to ignore this file.
|
60
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
61
|
+
|
62
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
63
|
+
# recommended. For more details, see:
|
64
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
65
|
+
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
66
|
+
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
67
|
+
config.disable_monkey_patching!
|
68
|
+
|
69
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
70
|
+
# be too noisy due to issues in dependencies.
|
71
|
+
config.warnings = true
|
72
|
+
|
73
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
74
|
+
# file, and it's useful to allow more verbose output when running an
|
75
|
+
# individual spec file.
|
76
|
+
if config.files_to_run.one?
|
77
|
+
# Use the documentation formatter for detailed output,
|
78
|
+
# unless a formatter has already been configured
|
79
|
+
# (e.g. via a command-line flag).
|
80
|
+
config.default_formatter = 'doc'
|
81
|
+
end
|
82
|
+
|
83
|
+
# Print the 10 slowest examples and example groups at the
|
84
|
+
# end of the spec run, to help surface which specs are running
|
85
|
+
# particularly slow.
|
86
|
+
config.profile_examples = 10
|
87
|
+
|
88
|
+
# Run specs in random order to surface order dependencies. If you find an
|
89
|
+
# order dependency and want to debug it, you can fix the order by providing
|
90
|
+
# the seed, which is printed after each run.
|
91
|
+
# --seed 1234
|
92
|
+
config.order = :random
|
93
|
+
|
94
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
95
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
96
|
+
# test failures related to randomization by passing the same `--seed` value
|
97
|
+
# as the one that triggered the failure.
|
98
|
+
Kernel.srand config.seed
|
99
|
+
=end
|
100
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
describe 'transports' do
|
5
|
+
include Farcall
|
6
|
+
|
7
|
+
it 'should provide debug transport' do
|
8
|
+
s1, s2 = Socket.pair(:UNIX, :STREAM, 0)
|
9
|
+
t1 = Farcall::DebugSocketStream.new s1, 0.01
|
10
|
+
t2 = Farcall::SocketStream.new s2
|
11
|
+
|
12
|
+
data = 'Not too long string'
|
13
|
+
data2 = 'the end'
|
14
|
+
t = Time.now
|
15
|
+
Thread.start {
|
16
|
+
t1.write data
|
17
|
+
t1.write data2
|
18
|
+
}
|
19
|
+
x = t2.read data.length
|
20
|
+
x.should == data
|
21
|
+
x = data2.length.times.map { t2.read }.join('')
|
22
|
+
x.should == data2
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should run json transport' do
|
26
|
+
s1, s2 = Socket.pair(:UNIX, :STREAM, 0)
|
27
|
+
|
28
|
+
j1 = Farcall::JsonTransport.new socket: s1
|
29
|
+
j2 = Farcall::JsonTransport.new socket: s2
|
30
|
+
|
31
|
+
j2.receive_data { |data|
|
32
|
+
j2.send_data({ echo: data })
|
33
|
+
}
|
34
|
+
|
35
|
+
results = []
|
36
|
+
j1.receive_data { |data|
|
37
|
+
results << data
|
38
|
+
}
|
39
|
+
|
40
|
+
j1.send_data({ foo: "bar" })
|
41
|
+
j1.send_data({ one: 2 })
|
42
|
+
sleep 0.01
|
43
|
+
j1.close
|
44
|
+
j2.close
|
45
|
+
|
46
|
+
results.should == [{ 'echo' => { 'foo' => 'bar' } }, { 'echo' => { 'one' => 2 } }]
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should run json transport with long delimiter' do
|
50
|
+
s1, s2 = Socket.pair(:UNIX, :STREAM, 0)
|
51
|
+
|
52
|
+
j1 = Farcall::JsonTransport.new socket: s1, delimiter: "\n\n\n\n"
|
53
|
+
j2 = Farcall::JsonTransport.new socket: s2, delimiter: "\n\n\n\n"
|
54
|
+
|
55
|
+
j2.receive_data { |data|
|
56
|
+
j2.send_data({ echo: data })
|
57
|
+
}
|
58
|
+
|
59
|
+
results = []
|
60
|
+
j1.receive_data { |data|
|
61
|
+
results << data
|
62
|
+
}
|
63
|
+
|
64
|
+
j1.send_data({ foo: "bar" })
|
65
|
+
j1.send_data({ one: 2 })
|
66
|
+
sleep 0.01
|
67
|
+
j1.close
|
68
|
+
j2.close
|
69
|
+
|
70
|
+
results.should == [{ 'echo' => { 'foo' => 'bar' } }, { 'echo' => { 'one' => 2 } }]
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: farcall
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- sergeych
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
description: |-
|
42
56
|
Can work with any transpot capable of conveing dictionaries (json, xml, bson, boss, yaml.
|
43
57
|
Incides some transports.
|
@@ -48,13 +62,20 @@ extensions: []
|
|
48
62
|
extra_rdoc_files: []
|
49
63
|
files:
|
50
64
|
- ".gitignore"
|
65
|
+
- ".rspec"
|
51
66
|
- Gemfile
|
52
67
|
- LICENSE.txt
|
53
68
|
- README.md
|
54
69
|
- Rakefile
|
55
70
|
- farcall.gemspec
|
56
71
|
- lib/farcall.rb
|
72
|
+
- lib/farcall/endpoint.rb
|
73
|
+
- lib/farcall/json_transport.rb
|
74
|
+
- lib/farcall/transport.rb
|
57
75
|
- lib/farcall/version.rb
|
76
|
+
- spec/endpoint_spec.rb
|
77
|
+
- spec/spec_helper.rb
|
78
|
+
- spec/transports_spec.rb
|
58
79
|
homepage: ''
|
59
80
|
licenses:
|
60
81
|
- MIT
|
@@ -79,4 +100,7 @@ rubygems_version: 2.4.5
|
|
79
100
|
signing_key:
|
80
101
|
specification_version: 4
|
81
102
|
summary: Simple, elegant and cross-platofrm RCP and RMI protocol
|
82
|
-
test_files:
|
103
|
+
test_files:
|
104
|
+
- spec/endpoint_spec.rb
|
105
|
+
- spec/spec_helper.rb
|
106
|
+
- spec/transports_spec.rb
|