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.
@@ -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