ione-rpc 1.0.0.pre0

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.
@@ -0,0 +1,153 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+
6
+ module Ione
7
+ module Rpc
8
+ describe Codec do
9
+ let :codec do
10
+ CodecSpec::JsonCodec.new
11
+ end
12
+
13
+ describe '#encode' do
14
+ let :object do
15
+ {'foo' => 'bar', 'baz' => 42}
16
+ end
17
+
18
+ let :encoded_message do
19
+ codec.encode(object, 42)
20
+ end
21
+
22
+ it 'encodes the version in the first byte' do
23
+ encoded_message[0, 1].unpack('c').should == [1]
24
+ end
25
+
26
+ it 'encodes the channel in the second byte' do
27
+ encoded_message[1, 1].unpack('c').should == [42]
28
+ end
29
+
30
+ it 'encodes the length of the frame in the following four bytes' do
31
+ encoded_message[2, 4].unpack('N').should == [22]
32
+ end
33
+
34
+ it 'encodes the object as JSON' do
35
+ encoded_message[6..-1].should == '{"foo":"bar","baz":42}'
36
+ end
37
+ end
38
+
39
+ describe '#decode' do
40
+ let :object do
41
+ {'foo' => 'bar', 'baz' => 42}
42
+ end
43
+
44
+ let :encoded_message do
45
+ %(\x01\x2a\x00\x00\x00\x16{"foo":"bar","baz":42})
46
+ end
47
+
48
+ it 'returns a partial message when there are less than five bytes' do
49
+ _, _, complete = codec.decode(Ione::ByteBuffer.new(encoded_message[0, 4]), nil)
50
+ complete.should be_false
51
+ end
52
+
53
+ it 'returns a partial message when the combined size of a previous partial message an new data is still less than the full frame size' do
54
+ buffer = Ione::ByteBuffer.new
55
+ buffer << encoded_message[0, 10]
56
+ message, _, _ = codec.decode(buffer, nil)
57
+ buffer << encoded_message[10, 4]
58
+ _, _, complete = codec.decode(buffer, message)
59
+ complete.should be_false
60
+ end
61
+
62
+ it 'returns a message and channel when it gets a full frame in one chunk' do
63
+ message, channel, complete = codec.decode(Ione::ByteBuffer.new(encoded_message), nil)
64
+ complete.should be_true
65
+ message.should == object
66
+ channel.should == 42
67
+ end
68
+
69
+ it 'returns a message and channel when it gets a full frame in chunks' do
70
+ buffer = Ione::ByteBuffer.new
71
+ buffer << encoded_message[0, 4]
72
+ message, _, _ = codec.decode(buffer, nil)
73
+ buffer << encoded_message[4, 10]
74
+ message, _, _ = codec.decode(buffer, message)
75
+ buffer << encoded_message[14..-1]
76
+ message, channel, _ = codec.decode(buffer, message)
77
+ message.should == object
78
+ channel.should == 42
79
+ end
80
+
81
+ it 'returns a message when it gets more bytes than needed' do
82
+ buffer = Ione::ByteBuffer.new
83
+ buffer << encoded_message[0, 10]
84
+ message, _ = codec.decode(buffer, message)
85
+ buffer << encoded_message[10..-1]
86
+ buffer << 'fooooo'
87
+ message, channel = codec.decode(buffer, message)
88
+ message.should == object
89
+ channel.should == 42
90
+ end
91
+ end
92
+
93
+ context 'when decoding and encoding' do
94
+ let :message do
95
+ {'foo' => 'bar', 'baz' => 42}
96
+ end
97
+
98
+ let :channel do
99
+ 42
100
+ end
101
+
102
+ let :frame do
103
+ %(\x01\x2a\x00\x00\x00\x16{"foo":"bar","baz":42})
104
+ end
105
+
106
+ it 'decoding a encoded frame returns the original message' do
107
+ frm = codec.encode(message, channel)
108
+ msg, ch, _ = codec.decode(Ione::ByteBuffer.new(frm), nil)
109
+ msg.should == message
110
+ ch.should == channel
111
+ end
112
+
113
+ it 'encoding a message gives the original frame' do
114
+ msg, ch, _ = codec.decode(Ione::ByteBuffer.new(frame), nil)
115
+ frm = codec.encode(msg, ch)
116
+ frm.should == frame
117
+ end
118
+ end
119
+ end
120
+
121
+ describe StandardCodec do
122
+ let :codec do
123
+ described_class.new(JSON)
124
+ end
125
+
126
+ describe '#encode' do
127
+ it 'calls #dump on the delegate' do
128
+ codec.encode({'foo' => 'bar'}, 0)[6..-1].should == '{"foo":"bar"}'
129
+ end
130
+ end
131
+
132
+ describe '#decode' do
133
+ it 'calls #load on the delegate' do
134
+ buffer = Ione::ByteBuffer.new(%(\x01\x00\x00\x00\x00\x0d{"foo":"bar"}))
135
+ message, _, _ = codec.decode(buffer, nil)
136
+ message.should eql('foo' => 'bar')
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ module CodecSpec
144
+ class JsonCodec < Ione::Rpc::Codec
145
+ def encode_message(message)
146
+ JSON.dump(message)
147
+ end
148
+
149
+ def decode_message(str)
150
+ JSON.load(str)
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,137 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+
6
+ shared_examples 'peers' do
7
+ context 'when setting up' do
8
+ it 'registers itself to receive notifications when there is new data' do
9
+ connection.data_listener.should_not be_nil
10
+ end
11
+
12
+ it 'registers itself to receive notifications when the connection closes' do
13
+ connection.closed_listener.should_not be_nil
14
+ end
15
+ end
16
+
17
+ context 'when receiving data' do
18
+ let :empty_frame do
19
+ double(:empty_frame, :partial? => true)
20
+ end
21
+
22
+ let :partial_frame do
23
+ double(:partial_frame, :partial? => true)
24
+ end
25
+
26
+ let :complete_frame do
27
+ double(:complete_frame, :partial? => false)
28
+ end
29
+
30
+ before do
31
+ channel = 9
32
+ codec.stub(:decode) do |buffer, previous_frame|
33
+ if buffer.empty?
34
+ [empty_frame, channel, false]
35
+ elsif buffer.index('FAKEPARTIALFRAME') == 0
36
+ buffer.read(16)
37
+ [partial_frame, channel, false]
38
+ elsif buffer.index('FAKEENDOFFRAME') == 0 && previous_frame == partial_frame
39
+ buffer.read(14)
40
+ [complete_frame, channel, true]
41
+ elsif buffer.index('FOO') == 0 || buffer.index('BAR') == 0 || buffer.index('BAZ') == 0
42
+ buffer.read(3)
43
+ [complete_frame, channel, true]
44
+ end
45
+ end
46
+ end
47
+
48
+ it 'does nothing when it\'s a partial frame' do
49
+ connection.data_listener.call('FAKEPARTIALFRAME')
50
+ end
51
+
52
+ it 'stiches together frames from fragments' do
53
+ connection.data_listener.call('FAKEPARTIALFRAME')
54
+ connection.data_listener.call('FAKEENDOFFRAME')
55
+ connection.written_bytes.should_not be_nil
56
+ end
57
+
58
+ it 'delivers the decoded frames to #handle_message' do
59
+ connection.data_listener.call('FAKEPARTIALFRAME')
60
+ connection.data_listener.call('FAKEENDOFFRAME')
61
+ peer.messages.should == [[complete_frame, 9]]
62
+ end
63
+
64
+ it 'delivers multiple frames to #handle_message' do
65
+ connection.data_listener.call('FOOBARBAZ')
66
+ peer.messages.should have(3).items
67
+ end
68
+ end
69
+
70
+ context 'when sending data' do
71
+ before do
72
+ codec.stub(:encode) do |message, channel|
73
+ "#{message}@#{channel}"
74
+ end
75
+ end
76
+
77
+ it 'writes the encoded frame to the connection' do
78
+ peer.send_message('FUZZBAZZ')
79
+ codec.should have_received(:encode).with('FUZZBAZZ', 0)
80
+ connection.written_bytes.should == 'FUZZBAZZ@0'
81
+ end
82
+ end
83
+
84
+ context 'when the connection closes' do
85
+ it 'calls the closed listeners' do
86
+ called1 = false
87
+ called2 = false
88
+ peer.on_closed { called1 = true }
89
+ peer.on_closed { called2 = true }
90
+ connection.closed_listener.call
91
+ called1.should be_true
92
+ called2.should be_true
93
+ end
94
+
95
+ it 'calls the closed listener with the close cause' do
96
+ cause = nil
97
+ peer.on_closed { |e| cause = e }
98
+ connection.closed_listener.call(StandardError.new('foo'))
99
+ cause.should == StandardError.new('foo')
100
+ end
101
+ end
102
+
103
+ context 'when closed' do
104
+ before do
105
+ connection.stub(:close)
106
+ end
107
+
108
+ it 'closes the connection' do
109
+ peer.close
110
+ connection.should have_received(:close)
111
+ end
112
+ end
113
+ end
114
+
115
+ module RpcSpec
116
+ class FakeConnection
117
+ attr_reader :written_bytes, :data_listener, :closed_listener, :host, :port
118
+
119
+ def initialize
120
+ @host = 'example.com'
121
+ @port = 9999
122
+ @written_bytes = ''
123
+ end
124
+
125
+ def write(bytes)
126
+ @written_bytes << bytes
127
+ end
128
+
129
+ def on_data(&listener)
130
+ @data_listener = listener
131
+ end
132
+
133
+ def on_closed(&listener)
134
+ @closed_listener = listener
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,214 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+
6
+ module Ione
7
+ module Rpc
8
+ describe Server do
9
+ let :server do
10
+ ServerSpec::TestServer.new(4321, codec, io_reactor: io_reactor, logger: logger)
11
+ end
12
+
13
+ let :codec do
14
+ double(:codec)
15
+ end
16
+
17
+ let :logger do
18
+ double(:logger)
19
+ end
20
+
21
+ let :acceptor do
22
+ double(:acceptor)
23
+ end
24
+
25
+ let :io_reactor do
26
+ double(:io_reactor)
27
+ end
28
+
29
+ before do
30
+ io_reactor.stub(:start).and_return(Ione::Future.resolved(io_reactor))
31
+ io_reactor.stub(:stop).and_return(Ione::Future.resolved(io_reactor))
32
+ io_reactor.stub(:bind) do |*args, &callback|
33
+ callback.call(acceptor)
34
+ Ione::Future.resolved
35
+ end
36
+ end
37
+
38
+ before do
39
+ acceptor.stub(:on_accept)
40
+ end
41
+
42
+ before do
43
+ codec.stub(:encode) { |msg, _| msg }
44
+ end
45
+
46
+ before do
47
+ logger.stub(:info)
48
+ end
49
+
50
+ describe '#port' do
51
+ it 'returns the port the server is listening on' do
52
+ server.port.should == 4321
53
+ end
54
+ end
55
+
56
+ describe '#start' do
57
+ it 'starts the reactor' do
58
+ server.start.value
59
+ io_reactor.should have_received(:start)
60
+ end
61
+
62
+ it 'starts a server on the specified port' do
63
+ server.start.value
64
+ io_reactor.should have_received(:bind).with('0.0.0.0', 4321, 5)
65
+ end
66
+
67
+ it 'starts a server that binds to the specified address' do
68
+ server = described_class.new(4321, codec, io_reactor: io_reactor, bind_address: '1.1.1.1')
69
+ server.start.value
70
+ io_reactor.should have_received(:bind).with('1.1.1.1', 4321, anything)
71
+ end
72
+
73
+ it 'uses the specified queue size' do
74
+ server = described_class.new(4321, codec, io_reactor: io_reactor, queue_size: 11)
75
+ server.start.value
76
+ io_reactor.should have_received(:bind).with(anything, anything, 11)
77
+ end
78
+
79
+ it 'returns a future that resolves to the server' do
80
+ server.start.value.should equal(server)
81
+ end
82
+
83
+ it 'logs the address and port when listening for connections' do
84
+ server.start.value
85
+ logger.should have_received(:info).with('Server listening for connections on 0.0.0.0:4321')
86
+ end
87
+ end
88
+
89
+ describe '#stop' do
90
+ it 'stops the reactor' do
91
+ server.stop.value
92
+ io_reactor.should have_received(:stop)
93
+ end
94
+
95
+ it 'returns a future that resolves to the server' do
96
+ server.stop.value.should equal(server)
97
+ end
98
+ end
99
+
100
+ shared_context 'client_connections' do
101
+ let :accept_listeners do
102
+ []
103
+ end
104
+
105
+ let :raw_connection do
106
+ double(:connection)
107
+ end
108
+
109
+ before do
110
+ raw_connection.stub(:on_data)
111
+ raw_connection.stub(:on_closed) { |&listener| raw_connection.stub(:closed_listener).and_return(listener) }
112
+ raw_connection.stub(:host).and_return('client.example.com')
113
+ raw_connection.stub(:port).and_return(34534)
114
+ raw_connection.stub(:write)
115
+ end
116
+
117
+ before do
118
+ acceptor.stub(:on_accept) do |&listener|
119
+ accept_listeners << listener
120
+ end
121
+ end
122
+
123
+ before do
124
+ server.start.value
125
+ end
126
+ end
127
+
128
+ context 'when a client connects' do
129
+ include_context 'client_connections'
130
+
131
+ before do
132
+ accept_listeners.first.call(raw_connection)
133
+ end
134
+
135
+ it 'calls #handle_connection with the client connection' do
136
+ server.connections.should have(1).item
137
+ server.connections.first.host.should == raw_connection.host
138
+ end
139
+
140
+ it 'logs that a client connected' do
141
+ logger.should have_received(:info).with('Connection from client.example.com:34534 accepted')
142
+ end
143
+
144
+ it 'logs when the client is disconnected' do
145
+ raw_connection.closed_listener.call
146
+ logger.should have_received(:info).with('Connection from client.example.com:34534 closed')
147
+ end
148
+ end
149
+
150
+ context 'when a client sends a request' do
151
+ include_context 'client_connections'
152
+
153
+ before do
154
+ accept_listeners.first.call(raw_connection)
155
+ end
156
+
157
+ it 'calls #handle_request with the request' do
158
+ server.connections.first.handle_message('FOOBAZ', 42)
159
+ server.received_messages.first.should == ['FOOBAZ', server.connections.first]
160
+ end
161
+
162
+ it 'responds to the same peer and channel when the future returned by #handle_request is resolved' do
163
+ promise = Promise.new
164
+ server.override_handle_request { promise.future }
165
+ peer = server.connections.first
166
+ peer.stub(:write_message)
167
+ peer.handle_message('FOOBAZ', 42)
168
+ peer.should_not have_received(:write_message)
169
+ promise.fulfill('BAZFOO')
170
+ peer.should have_received(:write_message).with('BAZFOO', 42)
171
+ end
172
+
173
+ it 'uses the codec to encode the response' do
174
+ codec.stub(:encode).with('BAZFOO', 42).and_return('42BAZFOO')
175
+ server.override_handle_request { Future.resolved('BAZFOO') }
176
+ peer = server.connections.first
177
+ peer.handle_message('FOOBAZ', 42)
178
+ raw_connection.should have_received(:write).with('42BAZFOO')
179
+ end
180
+
181
+ it 'handles that the server fails to process the request'
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ module ServerSpec
188
+ class TestServer < Ione::Rpc::Server
189
+ attr_reader :connections, :received_messages
190
+
191
+ def initialize(*)
192
+ super
193
+ @connections = []
194
+ @received_messages = []
195
+ end
196
+
197
+ def handle_connection(peer)
198
+ @connections << peer
199
+ end
200
+
201
+ def override_handle_request(&handler)
202
+ @request_handler = handler
203
+ end
204
+
205
+ def handle_request(request, peer)
206
+ @received_messages << [request, peer]
207
+ if @request_handler
208
+ @request_handler.call(request, peer)
209
+ else
210
+ super
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+
3
+ ENV['SERVER_HOST'] ||= '127.0.0.1'.freeze
4
+
5
+ require 'bundler/setup'
6
+
7
+ unless ENV['COVERAGE'] == 'no' || RUBY_ENGINE == 'rbx'
8
+ require 'coveralls'
9
+ require 'simplecov'
10
+
11
+ if ENV.include?('TRAVIS')
12
+ Coveralls.wear!
13
+ SimpleCov.formatter = Coveralls::SimpleCov::Formatter
14
+ end
15
+
16
+ SimpleCov.start do
17
+ add_group 'Source', 'lib'
18
+ add_group 'Unit tests', 'spec/ione'
19
+ add_group 'Integration tests', 'spec/integration'
20
+ end
21
+ end
22
+
23
+ require 'ione/rpc'
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ione-rpc
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.pre0
5
+ platform: ruby
6
+ authors:
7
+ - Theo Hultberg
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ione
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ description: A toolkit for building RPC layers using Ione
28
+ email:
29
+ - theo@iconara.net
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - .yardopts
35
+ - README.md
36
+ - lib/ione/rpc.rb
37
+ - lib/ione/rpc/client.rb
38
+ - lib/ione/rpc/client_peer.rb
39
+ - lib/ione/rpc/codec.rb
40
+ - lib/ione/rpc/peer.rb
41
+ - lib/ione/rpc/server.rb
42
+ - lib/ione/rpc/version.rb
43
+ - spec/ione/rpc/client_peer_spec.rb
44
+ - spec/ione/rpc/client_spec.rb
45
+ - spec/ione/rpc/codec_spec.rb
46
+ - spec/ione/rpc/peer_common.rb
47
+ - spec/ione/rpc/server_spec.rb
48
+ - spec/spec_helper.rb
49
+ homepage: http://github.com/iconara/ione-rpc
50
+ licenses:
51
+ - Apache License 2.0
52
+ metadata: {}
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.9.3
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - '>'
65
+ - !ruby/object:Gem::Version
66
+ version: 1.3.1
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 2.2.1
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: RPC toolkit for Ruby
73
+ test_files:
74
+ - spec/ione/rpc/client_peer_spec.rb
75
+ - spec/ione/rpc/client_spec.rb
76
+ - spec/ione/rpc/codec_spec.rb
77
+ - spec/ione/rpc/peer_common.rb
78
+ - spec/ione/rpc/server_spec.rb
79
+ - spec/spec_helper.rb
80
+ has_rdoc: