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.
- data/.gitignore +3 -1
- data/.travis.yml +7 -0
- data/CONTRIBUTORS +2 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +31 -0
- data/LICENSE +1 -1
- data/PROFILING.md +81 -0
- data/README.textile +34 -17
- data/TODO.todo +24 -0
- data/__init__.py +0 -0
- data/amq-protocol.gemspec +16 -11
- data/amqp_0.9.1_changes.json +1 -1
- data/bin/jenkins.sh +23 -0
- data/codegen.py +50 -16
- data/codegen_helpers.py +81 -26
- data/examples/00_manual_test.rb +117 -38
- data/irb.rb +76 -4
- data/lib/amq/hacks.rb +33 -0
- data/lib/amq/protocol.rb +3 -1472
- data/lib/amq/protocol/client.rb +2230 -0
- data/lib/amq/protocol/frame.rb +111 -38
- data/lib/amq/protocol/table.rb +89 -42
- data/lib/amq/protocol/version.rb +5 -0
- data/post-processing.rb +1 -0
- data/protocol.rb.pytemplate +217 -63
- data/spec/amq/hacks_spec.rb +39 -0
- data/spec/amq/protocol/basic_spec.rb +320 -0
- data/spec/amq/protocol/channel_spec.rb +117 -0
- data/spec/amq/protocol/confirm_spec.rb +44 -0
- data/spec/amq/protocol/connection_spec.rb +149 -0
- data/spec/amq/protocol/exchange_spec.rb +95 -0
- data/spec/amq/protocol/frame_spec.rb +93 -78
- data/spec/amq/protocol/method_spec.rb +40 -0
- data/spec/amq/protocol/queue_spec.rb +129 -0
- data/spec/amq/protocol/table_spec.rb +59 -36
- data/spec/amq/protocol/tx_spec.rb +58 -0
- data/spec/amq/protocol_spec.rb +572 -643
- data/spec/spec_helper.rb +25 -2
- data/tasks.rb +13 -8
- metadata +44 -25
- data/amq-protocol.pre.gemspec +0 -6
- data/examples/01_basics.rb +0 -14
- data/examples/02_eventmachine.rb +0 -4
data/lib/amq/protocol/frame.rb
CHANGED
@@ -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
|
7
|
-
TYPES_REVERSE = TYPES.
|
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,
|
16
|
-
raise
|
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
|
-
[
|
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
|
-
|
28
|
-
klass = CLASSES[
|
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.
|
34
|
-
|
35
|
-
|
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
|
-
|
38
|
-
|
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(
|
57
|
-
super(@id,
|
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
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
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 =
|
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 = {
|
95
|
-
|
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
|
data/lib/amq/protocol/table.rb
CHANGED
@@ -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
|
37
|
+
buffer << key.bytesize.chr + key
|
18
38
|
|
19
39
|
case value
|
20
|
-
when String
|
21
|
-
buffer
|
22
|
-
buffer
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
28
|
-
|
29
|
-
|
30
|
-
buffer
|
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
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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(
|
78
|
+
[buffer.bytesize].pack(PACK_UINT32) + buffer
|
53
79
|
end
|
54
80
|
|
55
81
|
def self.length(data)
|
56
|
-
data.unpack(
|
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(
|
87
|
+
size = data.unpack(PACK_UINT32).first
|
62
88
|
offset = 4
|
63
89
|
while offset < size
|
64
|
-
key_length = data
|
90
|
+
key_length = data.slice(offset, 1).unpack(PACK_CHAR).first
|
65
91
|
offset += 1
|
66
|
-
key = data
|
67
|
-
|
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
|
71
|
-
length = data
|
97
|
+
when TYPE_STRING
|
98
|
+
length = data.slice(offset, 4).unpack(PACK_UINT32).first
|
72
99
|
offset += 4
|
73
|
-
value = data
|
100
|
+
value = data.slice(offset, length)
|
74
101
|
offset += length
|
75
|
-
when
|
76
|
-
value = data
|
102
|
+
when TYPE_INTEGER
|
103
|
+
value = data.slice(offset, 4).unpack(PACK_UINT32).first
|
77
104
|
offset += 4
|
78
|
-
when
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
when
|
83
|
-
value
|
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
|
data/post-processing.rb
CHANGED
data/protocol.rb.pytemplate
CHANGED
@@ -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
|
6
|
+
# IN THE ROOT DIRECTORY OF THE AMQ-PROTOCOL REPOSITORY.<% import codegen_helpers as helpers %><% import re, codegen %>
|
7
7
|
|
8
|
-
|
9
|
-
|
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
|
15
|
-
DEFAULT_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
|
-
|
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
|
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
|
-
|
124
|
+
@classes = Array.new
|
61
125
|
|
62
|
-
def self.
|
63
|
-
@
|
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
|
-
|
135
|
+
if self == Protocol::Class
|
136
|
+
@classes << base
|
73
137
|
end
|
74
138
|
end
|
75
139
|
|
76
140
|
def self.classes
|
77
|
-
|
141
|
+
@classes
|
78
142
|
end
|
79
143
|
end
|
80
144
|
|
81
145
|
class Method
|
82
|
-
|
83
|
-
def self.
|
84
|
-
@
|
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
|
-
|
160
|
+
if self == Protocol::Method
|
161
|
+
@methods << base
|
98
162
|
end
|
99
163
|
end
|
100
164
|
|
101
165
|
def self.methods
|
102
|
-
|
166
|
+
@methods
|
103
167
|
end
|
104
168
|
|
105
|
-
def self.split_headers(user_headers
|
169
|
+
def self.split_headers(user_headers)
|
106
170
|
properties, headers = {}, {}
|
107
|
-
user_headers.
|
108
|
-
|
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
|
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
|
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
|
-
@
|
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
|
-
|
163
|
-
% for line in helpers.genSingleEncode(spec, "
|
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),)},
|
232
|
+
[${i}, ${"0x%04x" % ( 1 << (15-i),)}, buffer]
|
167
233
|
end
|
168
234
|
|
169
235
|
% endfor
|
170
236
|
|
171
237
|
% endif
|
172
238
|
|
173
|
-
|
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 =
|
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 = [
|
185
|
-
[
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
#
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
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
|
-
@
|
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
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
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
|
-
|
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
|
|