amq-protocol 0.0.1.pre

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