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