amq-protocol 0.0.1.pre → 0.5.0

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