amq-protocol 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|