amq-protocol 0.0.1.pre → 0.5.0

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.
@@ -2,44 +2,51 @@
2
2
 
3
3
  module AMQ
4
4
  module Protocol
5
+ SIMPLE_BYTE_PACK = 'c*'
5
6
  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) }
7
+ TYPES = {:method => 1, :headers => 2, :body => 3, :heartbeat => 8}.freeze
8
+ TYPES_REVERSE = TYPES.invert.freeze
9
+ TYPES_OPTIONS = TYPES.keys.freeze
10
+ CHANNEL_RANGE = (0..65535).freeze
11
+ FINAL_OCTET = "\xCE".freeze # 206
13
12
 
14
13
  # 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)
14
+ def self.encode(type, payload, channel)
15
+ raise RuntimeError.new("Channel has to be 0 or an integer in range 1..65535 but was #{channel.inspect}") unless CHANNEL_RANGE.include?(channel)
18
16
  raise RuntimeError.new("Payload can't be nil") if payload.nil?
19
- [TYPES[type], channel, payload.bytesize].pack("cnN") + payload + FINAL_OCTET
17
+ [find_type(type), channel, payload.bytesize].pack(PACK_CHAR_UINT16_UINT32) + payload.bytes.to_a.pack(SIMPLE_BYTE_PACK) + FINAL_OCTET
20
18
  end
21
19
 
22
20
  class << self
23
- alias_method :__new__, :new
21
+ alias_method :__new__, :new unless method_defined?(:__new__) # because of reloading
24
22
  end
25
23
 
26
24
  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?
25
+ type_id = find_type(original_type)
26
+ klass = CLASSES[type_id]
30
27
  klass.new(*args)
31
28
  end
32
29
 
33
- def self.decode(readable)
34
- header = readable.read(7)
35
- type_id, channel, size = header.unpack("cnN")
30
+ def self.find_type(type)
31
+ type_id = if Symbol === type then TYPES[type] else type end
32
+ raise FrameTypeError.new(TYPES_OPTIONS) if type == nil || !TYPES_REVERSE.has_key?(type_id)
33
+ type_id
34
+ end
35
+
36
+ def self.decode(*)
37
+ raise NotImplementedError.new <<-EOF
38
+ You are supposed to redefine this method, because it's dependent on used IO adapter.
39
+
40
+ This functionality is part of the https://github.com/ruby-amqp/amq-client library.
41
+ EOF
42
+ end
43
+
44
+ def self.decode_header(header)
45
+ raise EmptyResponseError if header == nil
46
+ type_id, channel, size = header.unpack(PACK_CHAR_UINT16_UINT32)
36
47
  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)
48
+ raise FrameTypeError.new(TYPES_OPTIONS) unless type
49
+ [type, channel, size]
43
50
  end
44
51
  end
45
52
 
@@ -47,33 +54,47 @@ module AMQ
47
54
  # Restore original new
48
55
  class << self
49
56
  alias_method :new, :__new__
57
+ undef_method :decode if method_defined?(:decode)
50
58
  end
51
59
 
52
60
  def self.id
53
61
  @id
54
62
  end
55
63
 
56
- def self.encode(channel, payload)
57
- super(@id, channel, payload)
64
+ def self.encode(payload, channel)
65
+ super(@id, payload, channel)
66
+ end
67
+
68
+ attr_accessor :channel
69
+ attr_reader :payload
70
+ def initialize(payload, channel)
71
+ @payload, @channel = payload, channel
58
72
  end
59
73
 
60
- attr_reader :channel, :size, :payload
61
- def initialize(channel, size, payload)
62
- @channel, @size, @payload = channel, size, payload
74
+ def size
75
+ @payload.bytesize
76
+ end
77
+
78
+ def encode
79
+ [self.class.id, @channel, self.size].pack(PACK_CHAR_UINT16_UINT32) + @payload.bytes.to_a.pack(SIMPLE_BYTE_PACK) + FINAL_OCTET
63
80
  end
64
81
  end
65
82
 
66
- # Example:
67
- # MethodFrame.encode(:method, 0, Connection::TuneOk.encode(0, 131072, 0))
68
83
  class MethodFrame < FrameSubclass
69
84
  @id = 1
70
85
 
71
86
  def method_class
72
- klass_id, method_id = self.payload.unpack("n2")
73
- index = klass_id << 16 | method_id
74
- AMQ::Protocol::METHODS[index]
87
+ @method_class ||= begin
88
+ klass_id, method_id = self.payload.unpack(PACK_UINT16_X2)
89
+ index = klass_id << 16 | method_id
90
+ AMQ::Protocol::METHODS[index]
91
+ end
75
92
  end
76
93
 
94
+ def final?
95
+ !self.method_class.has_content?
96
+ end # final?
97
+
77
98
  def decode_payload
78
99
  self.method_class.decode(@payload[4..-1])
79
100
  end
@@ -81,17 +102,69 @@ module AMQ
81
102
 
82
103
  class HeaderFrame < FrameSubclass
83
104
  @id = 2
105
+
106
+ def final?
107
+ false
108
+ end
109
+
110
+ def body_size
111
+ decode_payload
112
+ @body_size
113
+ end
114
+
115
+ def weight
116
+ decode_payload
117
+ @weight
118
+ end
119
+
120
+ def klass_id
121
+ decode_payload
122
+ @klass_id
123
+ end
124
+
125
+ def properties
126
+ decode_payload
127
+ @properties
128
+ end
129
+
130
+ def decode_payload
131
+ @decoded_payload ||= begin
132
+ @klass_id, @weight = @payload.unpack(PACK_UINT16_X2)
133
+ # the total size of the content body, that is, the sum of the body sizes for the
134
+ # following content body frames. Zero indicates that there are no content body frames.
135
+ # So this is NOT related to this very header frame!
136
+ @body_size = AMQ::Hacks.unpack_64_big_endian(@payload[4..11]).first
137
+ @data = @payload[12..-1]
138
+ @properties = Basic.decode_properties(@data)
139
+ end
140
+ end
84
141
  end
85
142
 
86
143
  class BodyFrame < FrameSubclass
87
144
  @id = 3
145
+
146
+ def decode_payload
147
+ @payload
148
+ end
88
149
  end
89
-
150
+
90
151
  class HeartbeatFrame < FrameSubclass
91
- @id = 4
152
+ @id = 8
153
+
154
+ def final?
155
+ true
156
+ end # final?
157
+
158
+ def self.encode
159
+ super(Protocol::EMPTY_STRING, 0)
160
+ end
92
161
  end
93
162
 
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) }
163
+ Frame::CLASSES = {
164
+ Frame::TYPES[:method] => MethodFrame,
165
+ Frame::TYPES[:headers] => HeaderFrame,
166
+ Frame::TYPES[:body] => BodyFrame,
167
+ Frame::TYPES[:heartbeat] => HeartbeatFrame
168
+ }
96
169
  end
97
170
  end
@@ -1,5 +1,10 @@
1
1
  # encoding: binary
2
2
 
3
+ require "amq/protocol/client"
4
+
5
+ # We will need to introduce concept of mappings, because
6
+ # AMQP 0.9, 0.9.1 and RabbitMQ uses different letters for entities
7
+ # http://dev.rabbitmq.com/wiki/Amqp091Errata#section_3
3
8
  module AMQ
4
9
  module Protocol
5
10
  class Table
@@ -9,80 +14,122 @@ module AMQ
9
14
  end
10
15
  end
11
16
 
17
+ TYPE_STRING = 'S'.freeze
18
+ TYPE_INTEGER = 'I'.freeze
19
+ TYPE_HASH = 'F'.freeze
20
+ TYPE_TIME = 'T'.freeze
21
+ TYPE_DECIMAL = 'D'.freeze
22
+ TYPE_BOOLEAN = 't'.freeze
23
+ TYPE_SIGNED_8BIT = 'b'.freeze
24
+ TYPE_SIGNED_16BIT = 's'.freeze
25
+ TYPE_SIGNED_64BIT = 'l'.freeze
26
+ TYPE_32BIT_FLOAT = 'f'.freeze
27
+ TYPE_64BIT_FLOAT = 'd'.freeze
28
+ TYPE_VOID = 'V'.freeze
29
+ TYPE_BYTE_ARRAY = 'x'.freeze
30
+ TEN = '10'.freeze
31
+
12
32
  def self.encode(table)
13
33
  buffer = String.new
14
34
  table ||= {}
15
35
  table.each do |key, value|
16
36
  key = key.to_s # it can be a symbol as well
17
- buffer += key.bytesize.chr + key
37
+ buffer << key.bytesize.chr + key
18
38
 
19
39
  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
40
+ when String then
41
+ buffer << TYPE_STRING
42
+ buffer << [value.bytesize].pack(PACK_UINT32)
43
+ buffer << value
44
+ when Integer then
45
+ buffer << TYPE_INTEGER
46
+ buffer << [value].pack(PACK_UINT32)
47
+ when TrueClass, FalseClass then
26
48
  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)
49
+ buffer << TYPE_INTEGER
50
+ buffer << [value].pack(PACK_UINT32)
51
+ when Hash then
52
+ buffer << TYPE_HASH # it will work as long as the encoding is ASCII-8BIT
53
+ buffer << self.encode(value)
54
+ when Time then
55
+ # TODO: encode timezone?
56
+ buffer << TYPE_TIME
57
+ buffer << [value.to_i].pack(PACK_INT64).reverse # Don't ask. It works.
31
58
  else
32
59
  # 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")
60
+ if defined?(BigDecimal) && value.is_a?(BigDecimal)
61
+ buffer << TYPE_DECIMAL
62
+ if value.exponent < 0
63
+ decimals = -value.exponent
64
+ # p [value.exponent] # normalize
65
+ raw = (value * (decimals ** 10)).to_i
66
+ #pieces.append(struct.pack('>cBI', 'D', decimals, raw)) # byte integer
67
+ buffer << [decimals + 1, raw].pack(PACK_UCHAR_UINT32) # somewhat like floating point
68
+ else
69
+ # per spec, the "decimals" octet is unsigned (!)
70
+ buffer << [0, value.to_i].pack(PACK_UCHAR_UINT32)
71
+ end
46
72
  else
47
73
  raise InvalidTableError.new(key, value)
48
74
  end
49
75
  end
50
76
  end
51
77
 
52
- [buffer.bytesize].pack(">N") + buffer
78
+ [buffer.bytesize].pack(PACK_UINT32) + buffer
53
79
  end
54
80
 
55
81
  def self.length(data)
56
- data.unpack("N").first
82
+ data.unpack(PACK_UINT32).first
57
83
  end
58
84
 
59
85
  def self.decode(data)
60
86
  table = Hash.new
61
- size = data.unpack("N").first
87
+ size = data.unpack(PACK_UINT32).first
62
88
  offset = 4
63
89
  while offset < size
64
- key_length = data[offset].unpack("c").first
90
+ key_length = data.slice(offset, 1).unpack(PACK_CHAR).first
65
91
  offset += 1
66
- key = data[offset...(offset += key_length)]
67
- type = data[offset]
92
+ key = data.slice(offset, key_length)
93
+ offset += key_length
94
+ type = data.slice(offset, 1)
68
95
  offset += 1
69
96
  case type
70
- when "S"
71
- length = data[offset...(offset + 4)].unpack("N").first
97
+ when TYPE_STRING
98
+ length = data.slice(offset, 4).unpack(PACK_UINT32).first
72
99
  offset += 4
73
- value = data[offset..(offset + length)]
100
+ value = data.slice(offset, length)
74
101
  offset += length
75
- when "I"
76
- value = data[offset...(offset + 4)].unpack("N").first
102
+ when TYPE_INTEGER
103
+ value = data.slice(offset, 4).unpack(PACK_UINT32).first
77
104
  offset += 4
78
- when "D"
79
- # TODO: decimal
80
- when "T"
81
- # TODO: timestamp
82
- when "F"
83
- value = self.decode(data[offset..-1])
105
+ when TYPE_DECIMAL
106
+ decimals, raw = data.slice(offset, 5).unpack(PACK_UCHAR_UINT32)
107
+ offset += 5
108
+ value = BigDecimal.new(raw.to_s) * (BigDecimal.new(TEN) ** -decimals)
109
+ when TYPE_TIME
110
+ # TODO: what is the first unpacked value??? Zone, maybe? It's 0, so it'd make sense.
111
+ timestamp = data.slice(offset, 8).unpack(PACK_UINT32_X2).last
112
+ value = Time.at(timestamp)
113
+ offset += 8
114
+ when TYPE_HASH
115
+ length = data.slice(offset, 4).unpack(PACK_UINT32).first
116
+ value = self.decode(data.slice(offset, length + 4))
117
+ offset += 4 + length
118
+ when TYPE_BOOLEAN
119
+ value = data.slice(offset, 2)
120
+ integer = value.unpack(PACK_CHAR).first # 0 or 1
121
+ value = integer == 1
122
+ offset += 1
123
+ when TYPE_SIGNED_8BIT then raise NotImplementedError.new
124
+ when TYPE_SIGNED_16BIT then raise NotImplementedError.new
125
+ when TYPE_SIGNED_64BIT then raise NotImplementedError.new
126
+ when TYPE_32BIT_FLOAT then raise NotImplementedError.new
127
+ when TYPE_64BIT_FLOAT then raise NotImplementedError.new
128
+ when TYPE_VOID
129
+ value = nil
130
+ when TYPE_BYTE_ARRAY
84
131
  else
85
- raise "Not a valid type: #{type.inspect}"
132
+ raise "Not a valid type: #{type.inspect}\nData: #{data.inspect}\nUnprocessed data: #{data[offset..-1].inspect}\nOffset: #{offset}\nTotal size: #{size}\nProcessed data: #{table.inspect}"
86
133
  end
87
134
  table[key] = value
88
135
  end
@@ -0,0 +1,5 @@
1
+ module AMQ
2
+ module Protocol
3
+ VERSION = "0.5.0".freeze
4
+ end # Protocol
5
+ end # AMQ
@@ -9,6 +9,7 @@ buffer = ARGF.inject(String.new) do |buffer, line|
9
9
  # line filters
10
10
  line.gsub!(/\s*\n$/, "\n")
11
11
  line.gsub!("'", '"')
12
+ line.gsub!('u"', '"') if line =~ /^\s*# \[/
12
13
 
13
14
  buffer += line
14
15
  end
@@ -3,44 +3,108 @@
3
3
 
4
4
  # THIS IS AN AUTOGENERATED FILE, DO NOT MODIFY
5
5
  # IT DIRECTLY ! FOR CHANGES, PLEASE UPDATE CODEGEN.PY
6
- # IN THE ROOT DIRECTORY OF THE AMQP-PROTOCOL REPOSITORY.<% import codegen_helpers as helpers %>
6
+ # IN THE ROOT DIRECTORY OF THE AMQ-PROTOCOL REPOSITORY.<% import codegen_helpers as helpers %><% import re, codegen %>
7
7
 
8
- require_relative "protocol/table.rb"
9
- require_relative "protocol/frame.rb"
8
+ require "amq/protocol/table"
9
+ require "amq/protocol/frame"
10
+ require "amq/hacks"
10
11
 
11
12
  module AMQ
12
13
  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}
14
+ PROTOCOL_VERSION = "${spec.major}.${spec.minor}.${spec.revision}".freeze
15
+ PREAMBLE = "${'AMQP\\x00\\x%02x\\x%02x\\x%02x' % (spec.major, spec.minor, spec.revision)}".freeze
16
+ DEFAULT_PORT = ${spec.port}
16
17
 
17
18
  # caching
18
19
  EMPTY_STRING = "".freeze
19
20
 
21
+ PACK_CHAR = 'c'.freeze
22
+ PACK_UINT16 = 'n'.freeze
23
+ PACK_UINT16_X2 = 'n2'.freeze
24
+ PACK_UINT32 = 'N'.freeze
25
+ PACK_UINT32_X2 = 'N2'.freeze
26
+ PACK_INT64 = 'q'.freeze
27
+ PACK_UCHAR_UINT32 = 'CN'.freeze
28
+ PACK_CHAR_UINT16_UINT32 = 'cnN'.freeze
29
+
20
30
  # @version 0.0.1
21
31
  # @return [Array] Collection of subclasses of AMQ::Protocol::Class.
22
32
  def self.classes
23
- Class.classes
33
+ Protocol::Class.classes
24
34
  end
25
35
 
26
36
  # @version 0.0.1
27
37
  # @return [Array] Collection of subclasses of AMQ::Protocol::Method.
28
38
  def self.methods
29
- Method.methods
39
+ Protocol::Method.methods
30
40
  end
31
41
 
32
42
  class Error < StandardError
33
- def initialize(message = "AMQP error")
43
+ DEFAULT_MESSAGE = "AMQP error".freeze
44
+
45
+ def self.inherited(subclass)
46
+ @_subclasses ||= []
47
+ @_subclasses << subclass
48
+ end # self.inherited(subclazz)
49
+
50
+ def self.subclasses_with_values
51
+ @_subclasses.select{ |k| defined?(k::VALUE) }
52
+ end # self.subclasses_with_values
53
+
54
+ def self.[](code) # TODO: rewrite more effectively
55
+ if result = subclasses_with_values.detect { |klass| klass::VALUE == code }
56
+ result
57
+ else
58
+ raise "No such exception class for code #{code}" unless result
59
+ end # if
60
+ end # self.[]
61
+
62
+ def initialize(message = self.class::DEFAULT_MESSAGE)
34
63
  super(message)
35
64
  end
36
65
  end
37
66
 
38
- class ConnectionError < Error
67
+ class FrameTypeError < Protocol::Error
39
68
  def initialize(types)
40
69
  super("Must be one of #{types.inspect}")
41
70
  end
42
71
  end
43
72
 
73
+ class EmptyResponseError < Protocol::Error
74
+ DEFAULT_MESSAGE = "Empty response received from the server."
75
+
76
+ def initialize(message = self.class::DEFAULT_MESSAGE)
77
+ super(message)
78
+ end
79
+ end
80
+
81
+ class BadResponseError < Protocol::Error
82
+ def initialize(argument, expected, actual)
83
+ super("Argument #{argument} has to be #{expected.inspect}, was #{data.inspect}")
84
+ end
85
+ end
86
+
87
+ class SoftError < Protocol::Error
88
+ def self.inherited(subclass)
89
+ Error.inherited(subclass)
90
+ end # self.inherited(subclass)
91
+ end
92
+
93
+ class HardError < Protocol::Error
94
+ def self.inherited(subclass)
95
+ Error.inherited(subclass)
96
+ end # self.inherited(subclass)
97
+ end
98
+
99
+ % for tuple in spec.constants:
100
+ % if tuple[2] == "soft-error" or tuple[2] == "hard-error":
101
+ class ${codegen.to_ruby_class_name(tuple[0])} < ${codegen.to_ruby_class_name(tuple[2])}
102
+ VALUE = ${tuple[1]}
103
+ end
104
+
105
+ % endif
106
+ % endfor
107
+
44
108
  # We don't instantiate the following classes,
45
109
  # as we don't actually need any per-instance state.
46
110
  # Also, this is pretty low-level functionality,
@@ -57,10 +121,10 @@ module AMQ
57
121
  # all these methods would become global which would
58
122
  # be a bad, bad thing to do.
59
123
  class Class
60
- @@classes = Array.new
124
+ @classes = Array.new
61
125
 
62
- def self.method
63
- @method
126
+ def self.method_id
127
+ @method_id
64
128
  end
65
129
 
66
130
  def self.name
@@ -68,20 +132,20 @@ module AMQ
68
132
  end
69
133
 
70
134
  def self.inherited(base)
71
- if self == Class
72
- @@classes << base
135
+ if self == Protocol::Class
136
+ @classes << base
73
137
  end
74
138
  end
75
139
 
76
140
  def self.classes
77
- @@classes
141
+ @classes
78
142
  end
79
143
  end
80
144
 
81
145
  class Method
82
- @@methods = Array.new
83
- def self.method
84
- @method
146
+ @methods = Array.new
147
+ def self.method_id
148
+ @method_id
85
149
  end
86
150
 
87
151
  def self.name
@@ -93,29 +157,30 @@ module AMQ
93
157
  end
94
158
 
95
159
  def self.inherited(base)
96
- if self == Method
97
- @@methods << base
160
+ if self == Protocol::Method
161
+ @methods << base
98
162
  end
99
163
  end
100
164
 
101
165
  def self.methods
102
- @@methods
166
+ @methods
103
167
  end
104
168
 
105
- def self.split_headers(user_headers, properties_set)
169
+ def self.split_headers(user_headers)
106
170
  properties, headers = {}, {}
107
- user_headers.iteritems.each do |key, value|
108
- if properties_set.has_key?(key)
171
+ user_headers.each do |key, value|
172
+ # key MUST be a symbol since symbols are not garbage-collected
173
+ if Basic::PROPERTIES.include?(key)
109
174
  properties[key] = value
110
175
  else
111
176
  headers[key] = value
112
177
  end
113
178
  end
114
179
 
115
- return props, headers
180
+ return [properties, headers]
116
181
  end
117
182
 
118
- def self.encode_body(body, frame_size)
183
+ def self.encode_body(body, channel, frame_size)
119
184
  # Spec is broken: Our errata says that it does define
120
185
  # something, but it just doesn't relate do method and
121
186
  # properties frames. Which makes it, well, suboptimal.
@@ -124,8 +189,9 @@ module AMQ
124
189
 
125
190
  Array.new.tap do |array|
126
191
  while body
127
- payload, body = body[0..limit], body[limit..-1]
128
- array << [0x03, payload]
192
+ payload, body = body[0, limit + 1], body[limit + 1, body.length - limit]
193
+ # array << [0x03, payload]
194
+ array << BodyFrame.new(payload, channel)
129
195
  end
130
196
  end
131
197
  end
@@ -145,9 +211,9 @@ module AMQ
145
211
  end
146
212
 
147
213
  % for klass in spec.classes :
148
- class ${klass.constant_name} < Class
214
+ class ${klass.constant_name} < Protocol::Class
149
215
  @name = "${klass.name}"
150
- @method = ${klass.index}
216
+ @method_id = ${klass.index}
151
217
 
152
218
  % if klass.fields: ## only the Basic class has fields (refered as properties in the JSON)
153
219
  PROPERTIES = [
@@ -159,21 +225,21 @@ module AMQ
159
225
  % for f in klass.fields:
160
226
  # <% i = klass.fields.index(f) %>1 << ${15 - i}
161
227
  def self.encode_${f.ruby_name}(value)
162
- pieces = []
163
- % for line in helpers.genSingleEncode(spec, "result", f.domain):
228
+ buffer = ''
229
+ % for line in helpers.genSingleEncode(spec, "value", f.domain):
164
230
  ${line}
165
231
  % endfor
166
- [${i}, ${"0x%04x" % ( 1 << (15-i),)}, result]
232
+ [${i}, ${"0x%04x" % ( 1 << (15-i),)}, buffer]
167
233
  end
168
234
 
169
235
  % endfor
170
236
 
171
237
  % endif
172
238
 
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 #}
239
+ ## TODO: not only basic, resp. in fact it's only this class, but not necessarily in the future, rather check if properties are empty #}
240
+ % if klass.name == "basic" :
174
241
  def self.encode_properties(body_size, properties)
175
- pieces = Array.new(14) { AMQ::Protocol::EMPTY_STRING }
176
- flags = 0
242
+ pieces, flags = [], 0
177
243
 
178
244
  properties.each do |key, value|
179
245
  i, f, result = self.send(:"encode_#{key}", value)
@@ -181,42 +247,96 @@ module AMQ
181
247
  pieces[i] = result
182
248
  end
183
249
 
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
250
+ # result = [${klass.index}, 0, body_size, flags].pack('n2Qn')
251
+ result = [${klass.index}, 0].pack(PACK_UINT16_X2)
252
+ result += AMQ::Hacks.pack_64_big_endian(body_size)
253
+ result += [flags].pack(PACK_UINT16)
254
+ result + pieces.join(EMPTY_STRING)
255
+ end
256
+
257
+ # THIS DECODES ONLY FLAGS
258
+ DECODE_PROPERTIES = {
259
+ % for f in klass.fields:
260
+ ${"0x%04x" % ( 1 << (15 - klass.fields.index(f)),)} => :${f.ruby_name},
261
+ % endfor
262
+ }
263
+
264
+ DECODE_PROPERTIES_TYPE = {
265
+ % for f in klass.fields:
266
+ ${"0x%04x" % ( 1 << (15 - klass.fields.index(f)),)} => :${spec.resolveDomain(f.domain)},
267
+ % endfor
268
+ }
269
+
270
+ # Hash doesn't give any guarantees on keys order, we will do it in a
271
+ # straightforward way
272
+ DECODE_PROPERTIES_KEYS = [
273
+ % for f in klass.fields:
274
+ ${"0x%04x" % ( 1 << (15 - klass.fields.index(f)),)},
275
+ % endfor
276
+ ]
277
+
278
+ def self.decode_properties(data)
279
+ offset, data_length, properties = 0, data.bytesize, {}
280
+
281
+ compressed_index = data[offset, 2].unpack(PACK_UINT16)[0]
282
+ offset += 2
283
+ while data_length > offset
284
+ DECODE_PROPERTIES_KEYS.each do |key|
285
+ next unless compressed_index >= key
286
+ compressed_index -= key
287
+ name = DECODE_PROPERTIES[key] || raise(RuntimeError.new("No property found for index #{index.inspect}!"))
288
+ case DECODE_PROPERTIES_TYPE[key]
289
+ when :shortstr
290
+ size = data[offset, 1].unpack(PACK_CHAR)[0]
291
+ offset += 1
292
+ result = data[offset, size]
293
+ when :octet
294
+ size = 1
295
+ result = data[offset, size].unpack(PACK_CHAR).first
296
+ when :timestamp
297
+ size = 8
298
+ result = Time.at(data[offset, size].unpack(PACK_UINT32_X2).last)
299
+ when :table
300
+ size = 4 + data[offset, 4].unpack(PACK_UINT32)[0]
301
+ result = Table.decode(data[offset, size])
302
+ end
303
+ properties[name] = result
304
+ offset += size
305
+ end
306
+ end
307
+
308
+ properties
309
+ end
201
310
  % endif
202
311
 
203
- % for method in klass.methods :
204
- class ${method.constant_name} < Method
312
+ % for method in klass.methods:
313
+ class ${method.constant_name} < Protocol::Method
205
314
  @name = "${klass.name}.${method.name}"
206
- @method = ${method.index}
315
+ @method_id = ${method.index}
207
316
  @index = ${method.binary()}
317
+ @packed_indexes = [${klass.index}, ${method.index}].pack(PACK_UINT16_X2).freeze
208
318
 
209
- % if method.accepted_by("client") :
319
+ % if (spec.type == "client" and method.accepted_by("client")) or (spec.type == "server" and method.accepted_by("server")):
210
320
  # @return
211
321
  def self.decode(data)
212
322
  offset = 0
213
323
  % for line in helpers.genDecodeMethodDefinition(spec, method):
214
324
  ${line}
215
325
  % endfor
326
+ % if (method.klass.name == "connection" or method.klass.name == "channel") and method.name == "close":
327
+ if reply_code.eql?(200)
328
+ self.new(${', '.join([f.ruby_name for f in method.arguments])})
329
+ else
330
+ raise Error[reply_code].new(reply_text)
331
+ end
332
+ % else:
216
333
  self.new(${', '.join([f.ruby_name for f in method.arguments])})
334
+ % endif
217
335
  end
218
336
 
337
+ % if len(method.arguments) > 0:
219
338
  attr_reader ${', '.join([":" + f.ruby_name for f in method.arguments])}
339
+ % endif
220
340
  def initialize(${', '.join([f.ruby_name for f in method.arguments])})
221
341
  % for f in method.arguments:
222
342
  @${f.ruby_name} = ${f.ruby_name}
@@ -224,16 +344,50 @@ module AMQ
224
344
  end
225
345
  % endif
226
346
 
227
- % if method.accepted_by("server"):
347
+ def self.has_content?
348
+ % if method.hasContent:
349
+ true
350
+ % else:
351
+ false
352
+ % endif
353
+ end
354
+
355
+ % if (spec.type == "client" and method.accepted_by("server")) or (spec.type == "server" and method.accepted_by("client")):
228
356
  # @return
229
357
  # ${method.params()}
230
- def self.encode(${(", ").join(method.args())})
231
- pieces = []
232
- pieces << [${klass.index}, ${method.index}].pack("n2")
358
+ % if klass.name == "connection":
359
+ def self.encode(${(", ").join(method.not_ignored_args())})
360
+ % else:
361
+ def self.encode(${(", ").join(["channel"] + method.not_ignored_args())})
362
+ % endif
363
+ % for argument in method.ignored_args():
364
+ ${codegen.convert_to_ruby(argument)}
365
+ % endfor
366
+ % if klass.name == "connection":
367
+ channel = 0
368
+ % endif
369
+ buffer = ''
370
+ buffer << @packed_indexes
233
371
  % for line in helpers.genEncodeMethodDefinition(spec, method):
234
372
  ${line}
235
373
  % endfor
236
- pieces.join("")
374
+ % if "payload" in method.args() or "user_headers" in method.args():
375
+ frames = [MethodFrame.new(buffer, channel)]
376
+ % if "user_headers" in method.args():
377
+ properties, headers = self.split_headers(user_headers)
378
+ # TODO: what shall I do with the headers?
379
+ if properties.nil? or properties.empty?
380
+ raise RuntimeError.new("Properties can not be empty!") # TODO: or can they?
381
+ end
382
+ properties_payload = Basic.encode_properties(payload.bytesize, properties)
383
+ frames << HeaderFrame.new(properties_payload, channel)
384
+ % endif
385
+ % if "payload" in method.args():
386
+ frames + self.encode_body(payload, channel, frame_size)
387
+ % endif
388
+ % else:
389
+ MethodFrame.new(buffer, channel)
390
+ % endif
237
391
  end
238
392
  % endif
239
393