celldee-bunny 0.0.1
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.
- data/README.markdown +43 -0
- data/Rakefile +12 -0
- data/examples/simple.rb +30 -0
- data/lib/amqp/buffer.rb +276 -0
- data/lib/amqp/client.rb +160 -0
- data/lib/amqp/frame.rb +62 -0
- data/lib/amqp/protocol.rb +158 -0
- data/lib/amqp/spec.rb +832 -0
- data/lib/amqp.rb +5 -0
- data/lib/bunny/exchange.rb +49 -0
- data/lib/bunny/header.rb +30 -0
- data/lib/bunny/logger.rb +89 -0
- data/lib/bunny/queue.rb +93 -0
- data/lib/bunny.rb +57 -0
- data/protocol/amqp-0.8.json +617 -0
- data/protocol/amqp-0.8.xml +3908 -0
- data/protocol/codegen.rb +173 -0
- data/spec/bunny_spec.rb +55 -0
- metadata +72 -0
data/README.markdown
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# bunny README
|
|
2
|
+
|
|
3
|
+
This project was started to enable me to interact with RabbitMQ using Ruby. It has borrowed heavily from two projects:
|
|
4
|
+
|
|
5
|
+
1. **amqp** by *tmm1* (http://github.com/tmm1/amqp/tree/master)
|
|
6
|
+
2. **carrot** by *famoseagle* (http://github.com/famoseagle/carrot/tree/master)
|
|
7
|
+
|
|
8
|
+
I will be creating tests, examples and generally tinkering, so please bear with me.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
require 'bunny'
|
|
13
|
+
|
|
14
|
+
b = Bunny.new(:logging => true)
|
|
15
|
+
|
|
16
|
+
# start a communication session with the amqp server
|
|
17
|
+
begin
|
|
18
|
+
b.start
|
|
19
|
+
rescue Exception => e
|
|
20
|
+
puts 'ERROR - Could not start a session: ' + e
|
|
21
|
+
exit
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# declare a queue
|
|
25
|
+
q = Queue.new(b.client, 'test1')
|
|
26
|
+
|
|
27
|
+
# create a direct exchange
|
|
28
|
+
exchange = Exchange.new(b.client, :direct, 'test_ex')
|
|
29
|
+
|
|
30
|
+
# bind the queue to the exchange
|
|
31
|
+
q.bind(exchange)
|
|
32
|
+
|
|
33
|
+
# publish a message to the exchange
|
|
34
|
+
exchange.publish('Hello everybody!')
|
|
35
|
+
|
|
36
|
+
# get message from the queue
|
|
37
|
+
msg = q.pop
|
|
38
|
+
|
|
39
|
+
puts 'This is the message: ' + msg + "\n\n"
|
|
40
|
+
|
|
41
|
+
## LICENSE
|
|
42
|
+
|
|
43
|
+
Copyright (c) 2009 Chris Duncan; Published under The MIT License, see License
|
data/Rakefile
ADDED
data/examples/simple.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
$:.unshift File.dirname(__FILE__) + '/../lib'
|
|
2
|
+
|
|
3
|
+
require 'bunny'
|
|
4
|
+
|
|
5
|
+
b = Bunny.new(:logging => true)
|
|
6
|
+
|
|
7
|
+
# start a communication session with the amqp server
|
|
8
|
+
begin
|
|
9
|
+
b.start
|
|
10
|
+
rescue Exception => e
|
|
11
|
+
puts 'ERROR - Could not start a session: ' + e
|
|
12
|
+
exit
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# declare a queue
|
|
16
|
+
q = Queue.new(b.client, 'test1')
|
|
17
|
+
|
|
18
|
+
# create a direct exchange
|
|
19
|
+
exchange = Exchange.new(b.client, :direct, 'test_ex')
|
|
20
|
+
|
|
21
|
+
# bind the queue to the exchange
|
|
22
|
+
q.bind(exchange)
|
|
23
|
+
|
|
24
|
+
# publish a message to the exchange
|
|
25
|
+
exchange.publish('Hello everybody!')
|
|
26
|
+
|
|
27
|
+
# get message from the queue
|
|
28
|
+
msg = q.pop
|
|
29
|
+
|
|
30
|
+
puts 'This is the message: ' + msg + "\n\n"
|
data/lib/amqp/buffer.rb
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
if [].map.respond_to? :with_index
|
|
2
|
+
class Array #:nodoc:
|
|
3
|
+
def enum_with_index
|
|
4
|
+
each.with_index
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
else
|
|
8
|
+
require 'enumerator'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module AMQP
|
|
12
|
+
class Buffer #:nodoc: all
|
|
13
|
+
class Overflow < StandardError; end
|
|
14
|
+
class InvalidType < StandardError; end
|
|
15
|
+
|
|
16
|
+
def initialize data = ''
|
|
17
|
+
@data = data
|
|
18
|
+
@pos = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
attr_reader :pos
|
|
22
|
+
|
|
23
|
+
def data
|
|
24
|
+
@data.clone
|
|
25
|
+
end
|
|
26
|
+
alias :contents :data
|
|
27
|
+
alias :to_s :data
|
|
28
|
+
|
|
29
|
+
def << data
|
|
30
|
+
@data << data.to_s
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def length
|
|
35
|
+
@data.length
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def empty?
|
|
39
|
+
pos == length
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def rewind
|
|
43
|
+
@pos = 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def read_properties *types
|
|
47
|
+
types.shift if types.first == :properties
|
|
48
|
+
|
|
49
|
+
i = 0
|
|
50
|
+
values = []
|
|
51
|
+
|
|
52
|
+
while props = read(:short)
|
|
53
|
+
(0..14).each do |n|
|
|
54
|
+
# no more property types
|
|
55
|
+
break unless types[i]
|
|
56
|
+
|
|
57
|
+
# if flag is set
|
|
58
|
+
if props & (1<<(15-n)) != 0
|
|
59
|
+
if types[i] == :bit
|
|
60
|
+
# bit values exist in flags only
|
|
61
|
+
values << true
|
|
62
|
+
else
|
|
63
|
+
# save type name for later reading
|
|
64
|
+
values << types[i]
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
# property not set or is false bit
|
|
68
|
+
values << (types[i] == :bit ? false : nil)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
i+=1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# bit(0) == 0 means no more property flags
|
|
75
|
+
break unless props & 1 == 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
values.map do |value|
|
|
79
|
+
value.is_a?(Symbol) ? read(value) : value
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def read *types
|
|
84
|
+
if types.first == :properties
|
|
85
|
+
return read_properties(*types)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
values = types.map do |type|
|
|
89
|
+
case type
|
|
90
|
+
when :octet
|
|
91
|
+
_read(1, 'C')
|
|
92
|
+
when :short
|
|
93
|
+
_read(2, 'n')
|
|
94
|
+
when :long
|
|
95
|
+
_read(4, 'N')
|
|
96
|
+
when :longlong
|
|
97
|
+
upper, lower = _read(8, 'NN')
|
|
98
|
+
upper << 32 | lower
|
|
99
|
+
when :shortstr
|
|
100
|
+
_read read(:octet)
|
|
101
|
+
when :longstr
|
|
102
|
+
_read read(:long)
|
|
103
|
+
when :timestamp
|
|
104
|
+
Time.at read(:longlong)
|
|
105
|
+
when :table
|
|
106
|
+
t = Hash.new
|
|
107
|
+
|
|
108
|
+
table = Buffer.new(read(:longstr))
|
|
109
|
+
until table.empty?
|
|
110
|
+
key, type = table.read(:shortstr, :octet)
|
|
111
|
+
key = key.intern
|
|
112
|
+
t[key] ||= case type
|
|
113
|
+
when 83 # 'S'
|
|
114
|
+
table.read(:longstr)
|
|
115
|
+
when 73 # 'I'
|
|
116
|
+
table.read(:long)
|
|
117
|
+
when 68 # 'D'
|
|
118
|
+
exp = table.read(:octet)
|
|
119
|
+
num = table.read(:long)
|
|
120
|
+
num / 10.0**exp
|
|
121
|
+
when 84 # 'T'
|
|
122
|
+
table.read(:timestamp)
|
|
123
|
+
when 70 # 'F'
|
|
124
|
+
table.read(:table)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
t
|
|
129
|
+
when :bit
|
|
130
|
+
if (@bits ||= []).empty?
|
|
131
|
+
val = read(:octet)
|
|
132
|
+
@bits = (0..7).map{|i| (val & 1<<i) != 0 }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
@bits.shift
|
|
136
|
+
else
|
|
137
|
+
raise InvalidType, "Cannot read data of type #{type}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
types.size == 1 ? values.first : values
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def write type, data
|
|
145
|
+
case type
|
|
146
|
+
when :octet
|
|
147
|
+
_write(data, 'C')
|
|
148
|
+
when :short
|
|
149
|
+
_write(data, 'n')
|
|
150
|
+
when :long
|
|
151
|
+
_write(data, 'N')
|
|
152
|
+
when :longlong
|
|
153
|
+
lower = data & 0xffffffff
|
|
154
|
+
upper = (data & ~0xffffffff) >> 32
|
|
155
|
+
_write([upper, lower], 'NN')
|
|
156
|
+
when :shortstr
|
|
157
|
+
data = (data || '').to_s
|
|
158
|
+
_write([data.length, data], 'Ca*')
|
|
159
|
+
when :longstr
|
|
160
|
+
if data.is_a? Hash
|
|
161
|
+
write(:table, data)
|
|
162
|
+
else
|
|
163
|
+
data = (data || '').to_s
|
|
164
|
+
_write([data.length, data], 'Na*')
|
|
165
|
+
end
|
|
166
|
+
when :timestamp
|
|
167
|
+
write(:longlong, data.to_i)
|
|
168
|
+
when :table
|
|
169
|
+
data ||= {}
|
|
170
|
+
write :longstr, (data.inject(Buffer.new) do |table, (key, value)|
|
|
171
|
+
table.write(:shortstr, key.to_s)
|
|
172
|
+
|
|
173
|
+
case value
|
|
174
|
+
when String
|
|
175
|
+
table.write(:octet, 83) # 'S'
|
|
176
|
+
table.write(:longstr, value.to_s)
|
|
177
|
+
when Fixnum
|
|
178
|
+
table.write(:octet, 73) # 'I'
|
|
179
|
+
table.write(:long, value)
|
|
180
|
+
when Float
|
|
181
|
+
table.write(:octet, 68) # 'D'
|
|
182
|
+
# XXX there's gotta be a better way to do this..
|
|
183
|
+
exp = value.to_s.split('.').last.length
|
|
184
|
+
num = value * 10**exp
|
|
185
|
+
table.write(:octet, exp)
|
|
186
|
+
table.write(:long, num)
|
|
187
|
+
when Time
|
|
188
|
+
table.write(:octet, 84) # 'T'
|
|
189
|
+
table.write(:timestamp, value)
|
|
190
|
+
when Hash
|
|
191
|
+
table.write(:octet, 70) # 'F'
|
|
192
|
+
table.write(:table, value)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
table
|
|
196
|
+
end)
|
|
197
|
+
when :bit
|
|
198
|
+
[*data].to_enum(:each_slice, 8).each{|bits|
|
|
199
|
+
write(:octet, bits.enum_with_index.inject(0){ |byte, (bit, i)|
|
|
200
|
+
byte |= 1<<i if bit
|
|
201
|
+
byte
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
when :properties
|
|
205
|
+
values = []
|
|
206
|
+
data.enum_with_index.inject(0) do |short, ((type, value), i)|
|
|
207
|
+
n = i % 15
|
|
208
|
+
last = i+1 == data.size
|
|
209
|
+
|
|
210
|
+
if (n == 0 and i != 0) or last
|
|
211
|
+
if data.size > i+1
|
|
212
|
+
short |= 1<<0
|
|
213
|
+
elsif last and value
|
|
214
|
+
values << [type,value]
|
|
215
|
+
short |= 1<<(15-n)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
write(:short, short)
|
|
219
|
+
short = 0
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
if value and !last
|
|
223
|
+
values << [type,value]
|
|
224
|
+
short |= 1<<(15-n)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
short
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
values.each do |type, value|
|
|
231
|
+
write(type, value) unless type == :bit
|
|
232
|
+
end
|
|
233
|
+
else
|
|
234
|
+
raise InvalidType, "Cannot write data of type #{type}"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
self
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def extract
|
|
241
|
+
begin
|
|
242
|
+
cur_data, cur_pos = @data.clone, @pos
|
|
243
|
+
yield self
|
|
244
|
+
rescue Overflow
|
|
245
|
+
@data, @pos = cur_data, cur_pos
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def _read(size, pack = nil)
|
|
251
|
+
if @data.is_a?(Client)
|
|
252
|
+
raw = @data.read(size)
|
|
253
|
+
return raw if raw.nil? or pack.nil?
|
|
254
|
+
return raw.unpack(pack).first
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if @pos + size > length
|
|
258
|
+
raise Overflow
|
|
259
|
+
else
|
|
260
|
+
data = @data[@pos,size]
|
|
261
|
+
@data[@pos,size] = ''
|
|
262
|
+
if pack
|
|
263
|
+
data = data.unpack(pack)
|
|
264
|
+
data = data.pop if data.size == 1
|
|
265
|
+
end
|
|
266
|
+
data
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def _write data, pack = nil
|
|
271
|
+
data = [*data].pack(pack) if pack
|
|
272
|
+
@data[@pos,0] = data
|
|
273
|
+
@pos += data.length
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
data/lib/amqp/client.rb
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
module AMQP
|
|
2
|
+
class Client
|
|
3
|
+
CONNECT_TIMEOUT = 1.0
|
|
4
|
+
RETRY_DELAY = 10.0
|
|
5
|
+
|
|
6
|
+
attr_reader :status
|
|
7
|
+
attr_accessor :channel, :host, :logging, :port, :ticket
|
|
8
|
+
|
|
9
|
+
class ServerDown < StandardError; end
|
|
10
|
+
class ProtocolError < StandardError; end
|
|
11
|
+
|
|
12
|
+
def initialize(opts = {})
|
|
13
|
+
@host = opts[:host] || 'localhost'
|
|
14
|
+
@port = opts[:port] || AMQP::PORT
|
|
15
|
+
@user = opts[:user] || 'guest'
|
|
16
|
+
@pass = opts[:pass] || 'guest'
|
|
17
|
+
@vhost = opts[:vhost] || '/'
|
|
18
|
+
@logging = opts[:logging] || false
|
|
19
|
+
@insist = opts[:insist]
|
|
20
|
+
@status = 'NOT CONNECTED'
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def send_frame(*args)
|
|
24
|
+
args.each do |data|
|
|
25
|
+
data.ticket = ticket if ticket and data.respond_to?(:ticket=)
|
|
26
|
+
data = data.to_frame(channel) unless data.is_a?(Frame)
|
|
27
|
+
data.channel = channel
|
|
28
|
+
|
|
29
|
+
log :send, data
|
|
30
|
+
write(data.to_s)
|
|
31
|
+
end
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def next_frame
|
|
36
|
+
frame = Frame.parse(buffer)
|
|
37
|
+
log :received, frame
|
|
38
|
+
frame
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def next_method
|
|
42
|
+
next_payload
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def next_payload
|
|
46
|
+
frame = next_frame
|
|
47
|
+
frame and frame.payload
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def close
|
|
51
|
+
send_frame(
|
|
52
|
+
Protocol::Channel::Close.new(:reply_code => 200, :reply_text => 'bye', :method_id => 0, :class_id => 0)
|
|
53
|
+
)
|
|
54
|
+
puts "Error closing channel #{channel}" unless next_method.is_a?(Protocol::Channel::CloseOk)
|
|
55
|
+
|
|
56
|
+
self.channel = 0
|
|
57
|
+
send_frame(
|
|
58
|
+
Protocol::Connection::Close.new(:reply_code => 200, :reply_text => 'Goodbye', :class_id => 0, :method_id => 0)
|
|
59
|
+
)
|
|
60
|
+
puts "Error closing connection" unless next_method.is_a?(Protocol::Connection::CloseOk)
|
|
61
|
+
|
|
62
|
+
close_socket
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def read(*args)
|
|
66
|
+
send_command(:read, *args)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def write(*args)
|
|
70
|
+
send_command(:write, *args)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def start_session
|
|
74
|
+
@channel = 0
|
|
75
|
+
write(HEADER)
|
|
76
|
+
write([1, 1, VERSION_MAJOR, VERSION_MINOR].pack('C4'))
|
|
77
|
+
raise ProtocolError, 'bad start connection' unless next_method.is_a?(Protocol::Connection::Start)
|
|
78
|
+
|
|
79
|
+
send_frame(
|
|
80
|
+
Protocol::Connection::StartOk.new(
|
|
81
|
+
{:platform => 'Ruby', :product => 'Bunny', :information => 'http://github.com/celldee/bunny', :version => VERSION},
|
|
82
|
+
'AMQPLAIN',
|
|
83
|
+
{:LOGIN => @user, :PASSWORD => @pass},
|
|
84
|
+
'en_US'
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if next_method.is_a?(Protocol::Connection::Tune)
|
|
89
|
+
send_frame(
|
|
90
|
+
Protocol::Connection::TuneOk.new( :channel_max => 0, :frame_max => 131072, :heartbeat => 0)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
send_frame(
|
|
95
|
+
Protocol::Connection::Open.new(:virtual_host => @vhost, :capabilities => '', :insist => @insist)
|
|
96
|
+
)
|
|
97
|
+
raise ProtocolError, 'Cannot open connection' unless next_method.is_a?(Protocol::Connection::OpenOk)
|
|
98
|
+
|
|
99
|
+
@channel = 1
|
|
100
|
+
send_frame(Protocol::Channel::Open.new)
|
|
101
|
+
raise ProtocolError, "Cannot open channel #{channel}" unless next_method.is_a?(Protocol::Channel::OpenOk)
|
|
102
|
+
|
|
103
|
+
send_frame(
|
|
104
|
+
Protocol::Access::Request.new(:realm => '/data', :read => true, :write => true, :active => true, :passive => true)
|
|
105
|
+
)
|
|
106
|
+
method = next_method
|
|
107
|
+
raise ProtocolError, 'Access denied' unless method.is_a?(Protocol::Access::RequestOk)
|
|
108
|
+
self.ticket = method.ticket
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def buffer
|
|
114
|
+
@buffer ||= Buffer.new(self)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def send_command(cmd, *args)
|
|
118
|
+
begin
|
|
119
|
+
socket.__send__(cmd, *args)
|
|
120
|
+
rescue Errno::EPIPE, IOError => e
|
|
121
|
+
raise ServerDown, e.message
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def socket
|
|
126
|
+
return @socket if @socket and not @socket.closed?
|
|
127
|
+
|
|
128
|
+
begin
|
|
129
|
+
# Attempt to connect.
|
|
130
|
+
@socket = timeout(CONNECT_TIMEOUT) do
|
|
131
|
+
TCPSocket.new(host, port)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if Socket.constants.include? 'TCP_NODELAY'
|
|
135
|
+
@socket.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
|
136
|
+
end
|
|
137
|
+
@status = 'CONNECTED'
|
|
138
|
+
rescue SocketError, SystemCallError, IOError, Timeout::Error => e
|
|
139
|
+
raise ServerDown, e.message
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
@socket
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def close_socket(reason=nil)
|
|
146
|
+
# Close the socket. The server is not considered dead.
|
|
147
|
+
@socket.close if @socket and not @socket.closed?
|
|
148
|
+
@socket = nil
|
|
149
|
+
@status = "NOT CONNECTED"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def log(*args)
|
|
153
|
+
return unless logging
|
|
154
|
+
require 'pp'
|
|
155
|
+
pp args
|
|
156
|
+
puts
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
end
|
|
160
|
+
end
|
data/lib/amqp/frame.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module AMQP
|
|
2
|
+
class Frame #:nodoc: all
|
|
3
|
+
def initialize payload = nil, channel = 0
|
|
4
|
+
@channel, @payload = channel, payload
|
|
5
|
+
end
|
|
6
|
+
attr_accessor :channel, :payload
|
|
7
|
+
|
|
8
|
+
def id
|
|
9
|
+
self.class::ID
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_binary
|
|
13
|
+
buf = Buffer.new
|
|
14
|
+
buf.write :octet, id
|
|
15
|
+
buf.write :short, channel
|
|
16
|
+
buf.write :longstr, payload
|
|
17
|
+
buf.write :octet, FOOTER
|
|
18
|
+
buf.rewind
|
|
19
|
+
buf
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
to_binary.to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def == frame
|
|
27
|
+
[ :id, :channel, :payload ].inject(true) do |eql, field|
|
|
28
|
+
eql and __send__(field) == frame.__send__(field)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Invalid < StandardError; end
|
|
33
|
+
|
|
34
|
+
class Method
|
|
35
|
+
def initialize payload = nil, channel = 0
|
|
36
|
+
super
|
|
37
|
+
unless @payload.is_a? Protocol::Class::Method or @payload.nil?
|
|
38
|
+
@payload = Protocol.parse(@payload)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class Header
|
|
44
|
+
def initialize payload = nil, channel = 0
|
|
45
|
+
super
|
|
46
|
+
unless @payload.is_a? Protocol::Header or @payload.nil?
|
|
47
|
+
@payload = Protocol::Header.new(@payload)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class Body; end
|
|
53
|
+
|
|
54
|
+
def self.parse buf
|
|
55
|
+
buf = Buffer.new(buf) unless buf.is_a? Buffer
|
|
56
|
+
buf.extract do
|
|
57
|
+
id, channel, payload, footer = buf.read(:octet, :short, :longstr, :octet)
|
|
58
|
+
Frame.types[id].new(payload, channel) if footer == FOOTER
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|