amq-protocol 0.0.1.pre
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/.gitignore +4 -0
- data/.gitmodules +3 -0
- data/.rspec +3 -0
- data/CHANGELOG +3 -0
- data/LICENSE +20 -0
- data/README.textile +32 -0
- data/TODO.todo +3 -0
- data/amq-protocol.gemspec +34 -0
- data/amq-protocol.pre.gemspec +6 -0
- data/amqp_0.9.1_changes.json +1 -0
- data/benchmark.rb +24 -0
- data/codegen.py +121 -0
- data/codegen_helpers.py +113 -0
- data/examples/00_manual_test.rb +60 -0
- data/examples/01_basics.rb +14 -0
- data/examples/02_eventmachine.rb +4 -0
- data/irb.rb +9 -0
- data/lib/amq/protocol.rb +1473 -0
- data/lib/amq/protocol/frame.rb +97 -0
- data/lib/amq/protocol/table.rb +94 -0
- data/post-processing.rb +24 -0
- data/protocol.rb.pytemplate +253 -0
- data/spec/amq/protocol/frame_spec.rb +82 -0
- data/spec/amq/protocol/table_spec.rb +46 -0
- data/spec/amq/protocol_spec.rb +888 -0
- data/spec/spec_helper.rb +8 -0
- data/tasks.rb +23 -0
- metadata +94 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
|
3
|
+
module AMQ
|
4
|
+
module Protocol
|
5
|
+
class Frame
|
6
|
+
TYPES = {method: 1, header: 2, body: 3, heartbeat: 4}
|
7
|
+
TYPES_REVERSE = TYPES.inject({}) { |hash, pair| hash.merge!(pair[1] => pair[0]) }
|
8
|
+
TYPES_OPTIONS = TYPES.keys
|
9
|
+
CHANNEL_RANGE = (0..65535)
|
10
|
+
FINAL_OCTET = "\xCE" # 206
|
11
|
+
|
12
|
+
TYPES.default_proc = lambda { |hash, key| key if (1..4).include?(key) }
|
13
|
+
|
14
|
+
# The channel number is 0 for all frames which are global to the connection and 1-65535 for frames that refer to specific channels.
|
15
|
+
def self.encode(type, channel, payload)
|
16
|
+
raise ConnectionError.new(TYPES_OPTIONS) unless TYPES_OPTIONS.include?(type) or (type = TYPES[type])
|
17
|
+
raise RuntimeError.new("Channel has to be 0 or an integer in range 1..65535") unless CHANNEL_RANGE.include?(channel)
|
18
|
+
raise RuntimeError.new("Payload can't be nil") if payload.nil?
|
19
|
+
[TYPES[type], channel, payload.bytesize].pack("cnN") + payload + FINAL_OCTET
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
alias_method :__new__, :new
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.new(original_type, *args)
|
27
|
+
type = TYPES[original_type]
|
28
|
+
klass = CLASSES[type]
|
29
|
+
raise "Type can be an integer in range 1..4 or #{TYPES_OPTIONS.inspect}, was #{original_type.inspect}" if klass.nil?
|
30
|
+
klass.new(*args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.decode(readable)
|
34
|
+
header = readable.read(7)
|
35
|
+
type_id, channel, size = header.unpack("cnN")
|
36
|
+
type = TYPES_REVERSE[type_id]
|
37
|
+
data = readable.read(size + 1)
|
38
|
+
payload, frame_end = data[0..-2], data[-1]
|
39
|
+
raise RuntimeError.new("Frame doesn't end with #{FINAL_OCTET} as it must, which means the size is miscalculated.") unless frame_end == FINAL_OCTET
|
40
|
+
# raise RuntimeError.new("invalid size: is #{payload.bytesize}, expected #{size}") if @payload.bytesize != size # We obviously can't test that, because we used read(size), so it's pointless.
|
41
|
+
raise ConnectionError.new(TYPES_OPTIONS) unless TYPES_OPTIONS.include?(type)
|
42
|
+
self.new(type, channel, size, payload)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class FrameSubclass < Frame
|
47
|
+
# Restore original new
|
48
|
+
class << self
|
49
|
+
alias_method :new, :__new__
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.id
|
53
|
+
@id
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.encode(channel, payload)
|
57
|
+
super(@id, channel, payload)
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_reader :channel, :size, :payload
|
61
|
+
def initialize(channel, size, payload)
|
62
|
+
@channel, @size, @payload = channel, size, payload
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Example:
|
67
|
+
# MethodFrame.encode(:method, 0, Connection::TuneOk.encode(0, 131072, 0))
|
68
|
+
class MethodFrame < FrameSubclass
|
69
|
+
@id = 1
|
70
|
+
|
71
|
+
def method_class
|
72
|
+
klass_id, method_id = self.payload.unpack("n2")
|
73
|
+
index = klass_id << 16 | method_id
|
74
|
+
AMQ::Protocol::METHODS[index]
|
75
|
+
end
|
76
|
+
|
77
|
+
def decode_payload
|
78
|
+
self.method_class.decode(@payload[4..-1])
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class HeaderFrame < FrameSubclass
|
83
|
+
@id = 2
|
84
|
+
end
|
85
|
+
|
86
|
+
class BodyFrame < FrameSubclass
|
87
|
+
@id = 3
|
88
|
+
end
|
89
|
+
|
90
|
+
class HeartbeatFrame < FrameSubclass
|
91
|
+
@id = 4
|
92
|
+
end
|
93
|
+
|
94
|
+
Frame::CLASSES = {method: MethodFrame, header: HeaderFrame, body: BodyFrame, heartbeat: HeaderFrame}
|
95
|
+
Frame::CLASSES.default_proc = lambda { |hash, key| hash[Frame::TYPES_REVERSE[key]] if (1..4).include?(key) }
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
|
3
|
+
module AMQ
|
4
|
+
module Protocol
|
5
|
+
class Table
|
6
|
+
class InvalidTableError < StandardError
|
7
|
+
def initialize(key, value)
|
8
|
+
super("Invalid table value on key #{key}: #{value.inspect} (#{value.class})")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.encode(table)
|
13
|
+
buffer = String.new
|
14
|
+
table ||= {}
|
15
|
+
table.each do |key, value|
|
16
|
+
key = key.to_s # it can be a symbol as well
|
17
|
+
buffer += key.bytesize.chr + key
|
18
|
+
|
19
|
+
case value
|
20
|
+
when String
|
21
|
+
buffer += ["S".ord, value.bytesize].pack(">cN")
|
22
|
+
buffer += value
|
23
|
+
when Integer
|
24
|
+
buffer += ["I".ord, value].pack(">cN")
|
25
|
+
when TrueClass, FalseClass
|
26
|
+
value = value ? 1 : 0
|
27
|
+
buffer += ["I".ord, value].pack(">cN")
|
28
|
+
when Hash
|
29
|
+
buffer += "F" # it will work as long as the encoding is ASCII-8BIT
|
30
|
+
buffer += self.encode(value)
|
31
|
+
else
|
32
|
+
# We don't want to require these libraries.
|
33
|
+
if const_defined?(:BigDecimal) && value.is_a?(BigDecimal)
|
34
|
+
# TODO
|
35
|
+
# value = value.normalize()
|
36
|
+
# if value._exp < 0:
|
37
|
+
# decimals = -value._exp
|
38
|
+
# raw = int(value * (decimal.Decimal(10) ** decimals))
|
39
|
+
# pieces.append(struct.pack('>cBI', 'D', decimals, raw))
|
40
|
+
# else:
|
41
|
+
# # per spec, the "decimals" octet is unsigned (!)
|
42
|
+
# pieces.append(struct.pack('>cBI', 'D', 0, int(value)))
|
43
|
+
elsif const_defined?(:DateTime) && value.is_a?(DateTime)
|
44
|
+
# TODO
|
45
|
+
# buffer += ["T", calendar.timegm(value.utctimetuple())].pack(">cQ")
|
46
|
+
else
|
47
|
+
raise InvalidTableError.new(key, value)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
[buffer.bytesize].pack(">N") + buffer
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.length(data)
|
56
|
+
data.unpack("N").first
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.decode(data)
|
60
|
+
table = Hash.new
|
61
|
+
size = data.unpack("N").first
|
62
|
+
offset = 4
|
63
|
+
while offset < size
|
64
|
+
key_length = data[offset].unpack("c").first
|
65
|
+
offset += 1
|
66
|
+
key = data[offset...(offset += key_length)]
|
67
|
+
type = data[offset]
|
68
|
+
offset += 1
|
69
|
+
case type
|
70
|
+
when "S"
|
71
|
+
length = data[offset...(offset + 4)].unpack("N").first
|
72
|
+
offset += 4
|
73
|
+
value = data[offset..(offset + length)]
|
74
|
+
offset += length
|
75
|
+
when "I"
|
76
|
+
value = data[offset...(offset + 4)].unpack("N").first
|
77
|
+
offset += 4
|
78
|
+
when "D"
|
79
|
+
# TODO: decimal
|
80
|
+
when "T"
|
81
|
+
# TODO: timestamp
|
82
|
+
when "F"
|
83
|
+
value = self.decode(data[offset..-1])
|
84
|
+
else
|
85
|
+
raise "Not a valid type: #{type.inspect}"
|
86
|
+
end
|
87
|
+
table[key] = value
|
88
|
+
end
|
89
|
+
|
90
|
+
table
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/post-processing.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby -i
|
2
|
+
# encoding: utf-8
|
3
|
+
|
4
|
+
# helpers
|
5
|
+
def pass; end
|
6
|
+
|
7
|
+
# main
|
8
|
+
buffer = ARGF.inject(String.new) do |buffer, line|
|
9
|
+
# line filters
|
10
|
+
line.gsub!(/\s*\n$/, "\n")
|
11
|
+
line.gsub!("'", '"')
|
12
|
+
|
13
|
+
buffer += line
|
14
|
+
end
|
15
|
+
|
16
|
+
# buffer filters
|
17
|
+
buffer.gsub!(/\n{2,}/m, "\n\n")
|
18
|
+
pass while buffer.gsub!(/(\n( *) end)\n{2,}(\2end)/m, "\\1\n\\3")
|
19
|
+
|
20
|
+
# Make sure there's only one \n at the end
|
21
|
+
pass while buffer.chomp!
|
22
|
+
buffer += "\n"
|
23
|
+
|
24
|
+
puts buffer
|
@@ -0,0 +1,253 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# encoding: binary
|
3
|
+
|
4
|
+
# THIS IS AN AUTOGENERATED FILE, DO NOT MODIFY
|
5
|
+
# IT DIRECTLY ! FOR CHANGES, PLEASE UPDATE CODEGEN.PY
|
6
|
+
# IN THE ROOT DIRECTORY OF THE AMQP-PROTOCOL REPOSITORY.<% import codegen_helpers as helpers %>
|
7
|
+
|
8
|
+
require_relative "protocol/table.rb"
|
9
|
+
require_relative "protocol/frame.rb"
|
10
|
+
|
11
|
+
module AMQ
|
12
|
+
module Protocol
|
13
|
+
PROTOCOL_VERSION = "${spec.major}.${spec.minor}.${spec.revision}"
|
14
|
+
PREAMBLE = "${'AMQP\\x00\\x%02x\\x%02x\\x%02x' % (spec.major, spec.minor, spec.revision)}"
|
15
|
+
DEFAULT_PORT = ${spec.port}
|
16
|
+
|
17
|
+
# caching
|
18
|
+
EMPTY_STRING = "".freeze
|
19
|
+
|
20
|
+
# @version 0.0.1
|
21
|
+
# @return [Array] Collection of subclasses of AMQ::Protocol::Class.
|
22
|
+
def self.classes
|
23
|
+
Class.classes
|
24
|
+
end
|
25
|
+
|
26
|
+
# @version 0.0.1
|
27
|
+
# @return [Array] Collection of subclasses of AMQ::Protocol::Method.
|
28
|
+
def self.methods
|
29
|
+
Method.methods
|
30
|
+
end
|
31
|
+
|
32
|
+
class Error < StandardError
|
33
|
+
def initialize(message = "AMQP error")
|
34
|
+
super(message)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class ConnectionError < Error
|
39
|
+
def initialize(types)
|
40
|
+
super("Must be one of #{types.inspect}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# We don't instantiate the following classes,
|
45
|
+
# as we don't actually need any per-instance state.
|
46
|
+
# Also, this is pretty low-level functionality,
|
47
|
+
# hence it should have a reasonable performance.
|
48
|
+
# As everyone knows, garbage collector in MRI performs
|
49
|
+
# really badly, which is another good reason for
|
50
|
+
# not creating any objects, but only use class as
|
51
|
+
# a struct. Creating classes is quite expensive though,
|
52
|
+
# but here the inheritance comes handy and mainly
|
53
|
+
# as we can't simply make a reference to a function,
|
54
|
+
# we can't use a hash or an object. I've been also
|
55
|
+
# considering to have just a bunch of methods, but
|
56
|
+
# here's the problem, that after we'd require this file,
|
57
|
+
# all these methods would become global which would
|
58
|
+
# be a bad, bad thing to do.
|
59
|
+
class Class
|
60
|
+
@@classes = Array.new
|
61
|
+
|
62
|
+
def self.method
|
63
|
+
@method
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.name
|
67
|
+
@name
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.inherited(base)
|
71
|
+
if self == Class
|
72
|
+
@@classes << base
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.classes
|
77
|
+
@@classes
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Method
|
82
|
+
@@methods = Array.new
|
83
|
+
def self.method
|
84
|
+
@method
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.name
|
88
|
+
@name
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.index
|
92
|
+
@index
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.inherited(base)
|
96
|
+
if self == Method
|
97
|
+
@@methods << base
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.methods
|
102
|
+
@@methods
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.split_headers(user_headers, properties_set)
|
106
|
+
properties, headers = {}, {}
|
107
|
+
user_headers.iteritems.each do |key, value|
|
108
|
+
if properties_set.has_key?(key)
|
109
|
+
properties[key] = value
|
110
|
+
else
|
111
|
+
headers[key] = value
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
return props, headers
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.encode_body(body, frame_size)
|
119
|
+
# Spec is broken: Our errata says that it does define
|
120
|
+
# something, but it just doesn't relate do method and
|
121
|
+
# properties frames. Which makes it, well, suboptimal.
|
122
|
+
# https://dev.rabbitmq.com/wiki/Amqp091Errata#section_11
|
123
|
+
limit = frame_size - 7 - 1
|
124
|
+
|
125
|
+
Array.new.tap do |array|
|
126
|
+
while body
|
127
|
+
payload, body = body[0..limit], body[limit..-1]
|
128
|
+
array << [0x03, payload]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# We can return different:
|
134
|
+
# - instantiate given subclass of Method
|
135
|
+
# - create an OpenStruct object
|
136
|
+
# - create a hash
|
137
|
+
# - yield params into the block rather than just return
|
138
|
+
# @api plugin
|
139
|
+
def self.instantiate(*args, &block)
|
140
|
+
self.new(*args, &block)
|
141
|
+
# or OpenStruct.new(args.first)
|
142
|
+
# or args.first
|
143
|
+
# or block.call(*args)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
% for klass in spec.classes :
|
148
|
+
class ${klass.constant_name} < Class
|
149
|
+
@name = "${klass.name}"
|
150
|
+
@method = ${klass.index}
|
151
|
+
|
152
|
+
% if klass.fields: ## only the Basic class has fields (refered as properties in the JSON)
|
153
|
+
PROPERTIES = [
|
154
|
+
% for field in klass.fields:
|
155
|
+
:${field.ruby_name}, # ${spec.resolveDomain(field.domain)}
|
156
|
+
% endfor
|
157
|
+
]
|
158
|
+
|
159
|
+
% for f in klass.fields:
|
160
|
+
# <% i = klass.fields.index(f) %>1 << ${15 - i}
|
161
|
+
def self.encode_${f.ruby_name}(value)
|
162
|
+
pieces = []
|
163
|
+
% for line in helpers.genSingleEncode(spec, "result", f.domain):
|
164
|
+
${line}
|
165
|
+
% endfor
|
166
|
+
[${i}, ${"0x%04x" % ( 1 << (15-i),)}, result]
|
167
|
+
end
|
168
|
+
|
169
|
+
% endfor
|
170
|
+
|
171
|
+
% endif
|
172
|
+
|
173
|
+
% if klass.name == "basic" : ## TODO: not only basic, resp. in fact it's only this class, but not necessarily in the future, rather check if properties are empty #}
|
174
|
+
def self.encode_properties(body_size, properties)
|
175
|
+
pieces = Array.new(14) { AMQ::Protocol::EMPTY_STRING }
|
176
|
+
flags = 0
|
177
|
+
|
178
|
+
properties.each do |key, value|
|
179
|
+
i, f, result = self.send(:"encode_#{key}", value)
|
180
|
+
flags |= f
|
181
|
+
pieces[i] = result
|
182
|
+
end
|
183
|
+
|
184
|
+
result = [CLASS_BASIC, 0, body_size, flags].pack("!HHQH")
|
185
|
+
[0x02, result, pieces.join("")].join("")
|
186
|
+
end
|
187
|
+
|
188
|
+
#def self.decode_properties
|
189
|
+
# print "def %s(data, offset):" % (c.decode,)
|
190
|
+
# print " props = {}"
|
191
|
+
# print " flags, = struct.unpack_from('!H', data, offset)"
|
192
|
+
# print " offset += 2"
|
193
|
+
# print " assert (flags & 0x01) == 0"
|
194
|
+
# for i, f in enumerate(c.fields):
|
195
|
+
# print " if (flags & 0x%04x): # 1 << %i" % (1 << (15-i), 15-i)
|
196
|
+
# fields = codegen_helpers.UnpackWrapper()
|
197
|
+
# fields.add(f.n, f.t)
|
198
|
+
# fields.do_print(" "*8, "props['%s']")
|
199
|
+
# print " return props, offset"
|
200
|
+
#end
|
201
|
+
% endif
|
202
|
+
|
203
|
+
% for method in klass.methods :
|
204
|
+
class ${method.constant_name} < Method
|
205
|
+
@name = "${klass.name}.${method.name}"
|
206
|
+
@method = ${method.index}
|
207
|
+
@index = ${method.binary()}
|
208
|
+
|
209
|
+
% if method.accepted_by("client") :
|
210
|
+
# @return
|
211
|
+
def self.decode(data)
|
212
|
+
offset = 0
|
213
|
+
% for line in helpers.genDecodeMethodDefinition(spec, method):
|
214
|
+
${line}
|
215
|
+
% endfor
|
216
|
+
self.new(${', '.join([f.ruby_name for f in method.arguments])})
|
217
|
+
end
|
218
|
+
|
219
|
+
attr_reader ${', '.join([":" + f.ruby_name for f in method.arguments])}
|
220
|
+
def initialize(${', '.join([f.ruby_name for f in method.arguments])})
|
221
|
+
% for f in method.arguments:
|
222
|
+
@${f.ruby_name} = ${f.ruby_name}
|
223
|
+
% endfor
|
224
|
+
end
|
225
|
+
% endif
|
226
|
+
|
227
|
+
% if method.accepted_by("server"):
|
228
|
+
# @return
|
229
|
+
# ${method.params()}
|
230
|
+
def self.encode(${(", ").join(method.args())})
|
231
|
+
pieces = []
|
232
|
+
pieces << [${klass.index}, ${method.index}].pack("n2")
|
233
|
+
% for line in helpers.genEncodeMethodDefinition(spec, method):
|
234
|
+
${line}
|
235
|
+
% endfor
|
236
|
+
pieces.join("")
|
237
|
+
end
|
238
|
+
% endif
|
239
|
+
|
240
|
+
end
|
241
|
+
|
242
|
+
% endfor
|
243
|
+
end
|
244
|
+
|
245
|
+
% endfor
|
246
|
+
|
247
|
+
METHODS = begin
|
248
|
+
Method.methods.inject(Hash.new) do |hash, klass|
|
249
|
+
hash.merge!(klass.index => klass)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|