cql-rb 1.0.0.pre0
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/README.md +13 -0
- data/bin/cqlexec +135 -0
- data/lib/cql.rb +11 -0
- data/lib/cql/client.rb +196 -0
- data/lib/cql/future.rb +176 -0
- data/lib/cql/io.rb +13 -0
- data/lib/cql/io/io_reactor.rb +351 -0
- data/lib/cql/protocol.rb +39 -0
- data/lib/cql/protocol/decoding.rb +156 -0
- data/lib/cql/protocol/encoding.rb +109 -0
- data/lib/cql/protocol/request_frame.rb +228 -0
- data/lib/cql/protocol/response_frame.rb +551 -0
- data/lib/cql/uuid.rb +46 -0
- data/lib/cql/version.rb +5 -0
- data/spec/cql/client_spec.rb +368 -0
- data/spec/cql/future_spec.rb +297 -0
- data/spec/cql/io/io_reactor_spec.rb +290 -0
- data/spec/cql/protocol/decoding_spec.rb +464 -0
- data/spec/cql/protocol/encoding_spec.rb +338 -0
- data/spec/cql/protocol/request_frame_spec.rb +359 -0
- data/spec/cql/protocol/response_frame_spec.rb +746 -0
- data/spec/cql/uuid_spec.rb +40 -0
- data/spec/integration/client_spec.rb +101 -0
- data/spec/integration/protocol_spec.rb +326 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/support/fake_io_reactor.rb +55 -0
- data/spec/support/fake_server.rb +95 -0
- metadata +87 -0
data/lib/cql/io.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Cql
|
4
|
+
module Io
|
5
|
+
IoError = Class.new(CqlError)
|
6
|
+
ConnectionError = Class.new(IoError)
|
7
|
+
NotRunningError = Class.new(CqlError)
|
8
|
+
ConnectionNotFoundError = Class.new(CqlError)
|
9
|
+
ConnectionBusyError = Class.new(CqlError)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'cql/io/io_reactor'
|
@@ -0,0 +1,351 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'resolv-replace'
|
5
|
+
|
6
|
+
|
7
|
+
module Cql
|
8
|
+
module Io
|
9
|
+
class IoReactor
|
10
|
+
def initialize(options={})
|
11
|
+
@connection_timeout = options[:connection_timeout] || 5
|
12
|
+
@lock = Mutex.new
|
13
|
+
@streams = []
|
14
|
+
@command_queue = []
|
15
|
+
@queue_signal_receiver, @queue_signal_sender = IO.pipe
|
16
|
+
@started_future = Future.new
|
17
|
+
@stopped_future = Future.new
|
18
|
+
@running = false
|
19
|
+
end
|
20
|
+
|
21
|
+
def running?
|
22
|
+
@running
|
23
|
+
end
|
24
|
+
|
25
|
+
def start
|
26
|
+
@lock.synchronize do
|
27
|
+
unless @running
|
28
|
+
@running = true
|
29
|
+
@streams << CommandDispatcher.new(@queue_signal_receiver, @command_queue, @lock, @streams)
|
30
|
+
@reactor_thread = Thread.start do
|
31
|
+
begin
|
32
|
+
@started_future.complete!
|
33
|
+
io_loop
|
34
|
+
@stopped_future.complete!
|
35
|
+
rescue => e
|
36
|
+
@stopped_future.fail!(e)
|
37
|
+
raise
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
@started_future
|
43
|
+
end
|
44
|
+
|
45
|
+
def stop
|
46
|
+
@running = false
|
47
|
+
@stopped_future
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_connection(host, port)
|
51
|
+
connection = NodeConnection.new(host, port, @connection_timeout)
|
52
|
+
future = connection.open
|
53
|
+
future.on_failure do
|
54
|
+
@lock.synchronize do
|
55
|
+
@streams.delete(connection)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
future.on_complete do
|
59
|
+
command_queue_push
|
60
|
+
end
|
61
|
+
@lock.synchronize do
|
62
|
+
@streams << connection
|
63
|
+
end
|
64
|
+
command_queue_push
|
65
|
+
future
|
66
|
+
end
|
67
|
+
|
68
|
+
def queue_request(request, connection_id=nil)
|
69
|
+
future = Future.new
|
70
|
+
command_queue_push(:request, request, future, connection_id)
|
71
|
+
future
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_event_listener(&listener)
|
75
|
+
command_queue_push(:event_listener, listener)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
PING_BYTE = "\0".freeze
|
81
|
+
|
82
|
+
def io_loop
|
83
|
+
while running?
|
84
|
+
read_ready_streams = @streams.select(&:connected?)
|
85
|
+
write_ready_streams = @streams.select(&:can_write?)
|
86
|
+
readables, writables, _ = IO.select(read_ready_streams, write_ready_streams, nil, 1)
|
87
|
+
readables && readables.each(&:handle_read)
|
88
|
+
writables && writables.each(&:handle_write)
|
89
|
+
@streams.each(&:ping)
|
90
|
+
end
|
91
|
+
ensure
|
92
|
+
stop
|
93
|
+
@streams.each do |stream|
|
94
|
+
begin
|
95
|
+
stream.close
|
96
|
+
rescue IOError
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def command_queue_push(*item)
|
102
|
+
if item && item.any?
|
103
|
+
@lock.synchronize do
|
104
|
+
@command_queue << item
|
105
|
+
end
|
106
|
+
end
|
107
|
+
@queue_signal_sender.write(PING_BYTE)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class NodeConnection
|
112
|
+
def initialize(*args)
|
113
|
+
@host, @port, @connection_timeout = args
|
114
|
+
@connected_future = Future.new
|
115
|
+
@io = nil
|
116
|
+
@addrinfo = nil
|
117
|
+
@write_buffer = ''
|
118
|
+
@read_buffer = ''
|
119
|
+
@current_frame = Protocol::ResponseFrame.new(@read_buffer)
|
120
|
+
@response_tasks = [nil] * 128
|
121
|
+
@event_listeners = []
|
122
|
+
end
|
123
|
+
|
124
|
+
def open
|
125
|
+
@connection_started_at = Time.now
|
126
|
+
begin
|
127
|
+
addrinfo = Socket.getaddrinfo(@host, @port, Socket::AF_INET, Socket::SOCK_STREAM)
|
128
|
+
_, port, _, ip, address_family, socket_type = addrinfo.first
|
129
|
+
@sockaddr = Socket.sockaddr_in(port, ip)
|
130
|
+
@io = Socket.new(address_family, socket_type, 0)
|
131
|
+
@io.connect_nonblock(@sockaddr)
|
132
|
+
rescue Errno::EINPROGRESS
|
133
|
+
# ok
|
134
|
+
rescue SystemCallError, SocketError => e
|
135
|
+
fail_connection!(e)
|
136
|
+
end
|
137
|
+
@connected_future
|
138
|
+
end
|
139
|
+
|
140
|
+
def connection_id
|
141
|
+
self.object_id
|
142
|
+
end
|
143
|
+
|
144
|
+
def to_io
|
145
|
+
@io
|
146
|
+
end
|
147
|
+
|
148
|
+
def on_event(&listener)
|
149
|
+
@event_listeners << listener
|
150
|
+
end
|
151
|
+
|
152
|
+
def ping
|
153
|
+
if @io && connecting? && (Time.now - @connection_started_at > @connection_timeout)
|
154
|
+
fail_connection!
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def connected?
|
159
|
+
@io && !connecting?
|
160
|
+
end
|
161
|
+
|
162
|
+
def has_capacity?
|
163
|
+
!!next_stream_id && connected?
|
164
|
+
end
|
165
|
+
|
166
|
+
def can_write?
|
167
|
+
@io && (!@write_buffer.empty? || connecting?)
|
168
|
+
end
|
169
|
+
|
170
|
+
def perform_request(request, future)
|
171
|
+
stream_id = next_stream_id
|
172
|
+
Protocol::RequestFrame.new(request, stream_id).write(@write_buffer)
|
173
|
+
@response_tasks[stream_id] = future
|
174
|
+
end
|
175
|
+
|
176
|
+
def handle_read
|
177
|
+
new_bytes = @io.read_nonblock(2**16)
|
178
|
+
@current_frame << new_bytes
|
179
|
+
while @current_frame.complete?
|
180
|
+
stream_id = @current_frame.stream_id
|
181
|
+
if stream_id == EVENT_STREAM_ID
|
182
|
+
@event_listeners.each { |listener| listener.call(@current_frame.body) }
|
183
|
+
elsif @response_tasks[stream_id]
|
184
|
+
@response_tasks[stream_id].complete!([@current_frame.body, connection_id])
|
185
|
+
@response_tasks[stream_id] = nil
|
186
|
+
else
|
187
|
+
# TODO dropping the request on the floor here, but we didn't send it
|
188
|
+
end
|
189
|
+
@current_frame = Protocol::ResponseFrame.new(@read_buffer)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def handle_write
|
194
|
+
if connecting?
|
195
|
+
handle_connected
|
196
|
+
else
|
197
|
+
bytes_written = @io.write_nonblock(@write_buffer)
|
198
|
+
@write_buffer.slice!(0, bytes_written)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def close
|
203
|
+
if @io
|
204
|
+
@io.close
|
205
|
+
if connecting?
|
206
|
+
succeed_connection!
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def to_s
|
212
|
+
state = begin
|
213
|
+
if connected? then 'connected'
|
214
|
+
elsif connecting? then 'connecting'
|
215
|
+
else 'not connected'
|
216
|
+
end
|
217
|
+
end
|
218
|
+
%<NodeConnection(#{@host}:#{@port}, #{state})>
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
|
223
|
+
EVENT_STREAM_ID = -1
|
224
|
+
|
225
|
+
def connecting?
|
226
|
+
!@connected_future.complete?
|
227
|
+
end
|
228
|
+
|
229
|
+
def handle_connected
|
230
|
+
@io.connect_nonblock(@sockaddr)
|
231
|
+
succeed_connection!
|
232
|
+
rescue Errno::EISCONN
|
233
|
+
# ok
|
234
|
+
succeed_connection!
|
235
|
+
rescue SystemCallError, SocketError => e
|
236
|
+
fail_connection!(e)
|
237
|
+
end
|
238
|
+
|
239
|
+
def succeed_connection!
|
240
|
+
@connected_future.complete!(connection_id)
|
241
|
+
end
|
242
|
+
|
243
|
+
def fail_connection!(e=nil)
|
244
|
+
message = "Could not connect to #{@host}:#{@port}"
|
245
|
+
message << ": #{e.message} (#{e.class.name})" if e
|
246
|
+
error = ConnectionError.new(message)
|
247
|
+
error.set_backtrace(e.backtrace) if e
|
248
|
+
@connected_future.fail!(error)
|
249
|
+
@io.close if @io
|
250
|
+
@io = nil
|
251
|
+
end
|
252
|
+
|
253
|
+
def next_stream_id
|
254
|
+
@response_tasks.each_with_index do |task, index|
|
255
|
+
return index if task.nil?
|
256
|
+
end
|
257
|
+
nil
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
class CommandDispatcher
|
262
|
+
def initialize(*args)
|
263
|
+
@io, @command_queue, @queue_lock, @node_connections = args
|
264
|
+
end
|
265
|
+
|
266
|
+
def connection_id
|
267
|
+
-1
|
268
|
+
end
|
269
|
+
|
270
|
+
def to_io
|
271
|
+
@io
|
272
|
+
end
|
273
|
+
|
274
|
+
def connected?
|
275
|
+
true
|
276
|
+
end
|
277
|
+
|
278
|
+
def has_capacity?
|
279
|
+
false
|
280
|
+
end
|
281
|
+
|
282
|
+
def can_write?
|
283
|
+
false
|
284
|
+
end
|
285
|
+
|
286
|
+
def on_event; end
|
287
|
+
|
288
|
+
def ping
|
289
|
+
if can_deliver_command?
|
290
|
+
deliver_commands
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def handle_read
|
295
|
+
if @io.read_nonblock(1)
|
296
|
+
deliver_commands
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def handle_write
|
301
|
+
end
|
302
|
+
|
303
|
+
def close
|
304
|
+
@io.close
|
305
|
+
end
|
306
|
+
|
307
|
+
def to_s
|
308
|
+
%(CommandDispatcher)
|
309
|
+
end
|
310
|
+
|
311
|
+
private
|
312
|
+
|
313
|
+
def can_deliver_command?
|
314
|
+
@node_connections.any?(&:has_capacity?) && @command_queue.size > 0
|
315
|
+
end
|
316
|
+
|
317
|
+
def next_command
|
318
|
+
@queue_lock.synchronize do
|
319
|
+
if can_deliver_command?
|
320
|
+
return @command_queue.shift
|
321
|
+
end
|
322
|
+
end
|
323
|
+
nil
|
324
|
+
end
|
325
|
+
|
326
|
+
def deliver_commands
|
327
|
+
while (command = next_command)
|
328
|
+
case command.shift
|
329
|
+
when :event_listener
|
330
|
+
listener = command.shift
|
331
|
+
@node_connections.each { |c| c.on_event(&listener) }
|
332
|
+
else
|
333
|
+
request, future, connection_id = command
|
334
|
+
if connection_id
|
335
|
+
connection = @node_connections.find { |c| c.connection_id == connection_id }
|
336
|
+
if connection && connection.has_capacity?
|
337
|
+
connection.perform_request(request, future)
|
338
|
+
elsif connection
|
339
|
+
future.fail!(ConnectionBusyError.new("Connection ##{connection_id} is busy"))
|
340
|
+
else
|
341
|
+
future.fail!(ConnectionNotFoundError.new("Connection ##{connection_id} does not exist"))
|
342
|
+
end
|
343
|
+
else
|
344
|
+
@node_connections.select(&:has_capacity?).sample.perform_request(request, future)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
data/lib/cql/protocol.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Cql
|
4
|
+
module Protocol
|
5
|
+
ProtocolError = Class.new(CqlError)
|
6
|
+
DecodingError = Class.new(ProtocolError)
|
7
|
+
EncodingError = Class.new(ProtocolError)
|
8
|
+
InvalidStreamIdError = Class.new(ProtocolError)
|
9
|
+
UnsupportedOperationError = Class.new(ProtocolError)
|
10
|
+
UnsupportedFrameTypeError = Class.new(ProtocolError)
|
11
|
+
UnsupportedResultKindError = Class.new(ProtocolError)
|
12
|
+
UnsupportedColumnTypeError = Class.new(ProtocolError)
|
13
|
+
UnsupportedEventTypeError = Class.new(ProtocolError)
|
14
|
+
|
15
|
+
CONSISTENCIES = [:any, :one, :two, :three, :quorum, :all, :local_quorum, :each_quorum].freeze
|
16
|
+
|
17
|
+
module Formats
|
18
|
+
CHAR_FORMAT = 'c'.freeze
|
19
|
+
DOUBLE_FORMAT = 'G'.freeze
|
20
|
+
FLOAT_FORMAT = 'g'.freeze
|
21
|
+
INT_FORMAT = 'N'.freeze
|
22
|
+
SHORT_FORMAT = 'n'.freeze
|
23
|
+
|
24
|
+
BYTES_FORMAT = 'C*'.freeze
|
25
|
+
TWO_INTS_FORMAT = 'NN'.freeze
|
26
|
+
HEADER_FORMAT = 'c4N'.freeze
|
27
|
+
end
|
28
|
+
|
29
|
+
module Constants
|
30
|
+
TRUE_BYTE = "\x01".freeze
|
31
|
+
FALSE_BYTE = "\x00".freeze
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
require 'cql/protocol/encoding'
|
37
|
+
require 'cql/protocol/decoding'
|
38
|
+
require 'cql/protocol/response_frame'
|
39
|
+
require 'cql/protocol/request_frame'
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Cql
|
4
|
+
module Protocol
|
5
|
+
module Decoding
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def read_byte!(buffer)
|
9
|
+
raise DecodingError, 'No byte available to decode' if buffer.empty?
|
10
|
+
b = buffer.slice!(0, 1)
|
11
|
+
b.getbyte(0)
|
12
|
+
end
|
13
|
+
|
14
|
+
def read_varint!(buffer, length=buffer.length, signed=true)
|
15
|
+
raise DecodingError, "Length #{length} specifed but only #{buffer.size} bytes given" if buffer.size < length
|
16
|
+
bytes = buffer.slice!(0, length)
|
17
|
+
n = 0
|
18
|
+
bytes.each_byte do |b|
|
19
|
+
n = (n << 8) | b
|
20
|
+
end
|
21
|
+
if signed && bytes.getbyte(0) & 0x80 == 0x80
|
22
|
+
n -= 2**(bytes.length * 8)
|
23
|
+
end
|
24
|
+
n
|
25
|
+
end
|
26
|
+
|
27
|
+
def read_decimal!(buffer, length=buffer.length)
|
28
|
+
raise DecodingError, "Length #{length} specifed but only #{buffer.size} bytes given" if buffer.size < length
|
29
|
+
size = read_int!(buffer)
|
30
|
+
number_bytes = buffer.slice!(0, length - 4)
|
31
|
+
number_string = read_varint!(number_bytes).to_s
|
32
|
+
fraction_string = number_string[0, number_string.length - size] << DECIMAL_POINT << number_string[number_string.length - size, number_string.length]
|
33
|
+
BigDecimal.new(fraction_string)
|
34
|
+
end
|
35
|
+
|
36
|
+
def read_long!(buffer)
|
37
|
+
raise DecodingError, "Need eight bytes to decode long, only #{buffer.size} bytes given" if buffer.size < 8
|
38
|
+
top, bottom = buffer.slice!(0, 8).unpack(Formats::TWO_INTS_FORMAT)
|
39
|
+
(top << 32) | bottom
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_double!(buffer)
|
43
|
+
raise DecodingError, "Need eight bytes to decode double, only #{buffer.size} bytes given" if buffer.size < 8
|
44
|
+
buffer.slice!(0, 8).unpack(Formats::DOUBLE_FORMAT).first
|
45
|
+
end
|
46
|
+
|
47
|
+
def read_float!(buffer)
|
48
|
+
raise DecodingError, "Need four bytes to decode float, only #{buffer.size} bytes given" if buffer.size < 4
|
49
|
+
buffer.slice!(0, 4).unpack(Formats::FLOAT_FORMAT).first
|
50
|
+
end
|
51
|
+
|
52
|
+
def read_int!(buffer)
|
53
|
+
raise DecodingError, "Need four bytes to decode an int, only #{buffer.size} bytes given" if buffer.size < 4
|
54
|
+
buffer.slice!(0, 4).unpack(Formats::INT_FORMAT).first
|
55
|
+
end
|
56
|
+
|
57
|
+
def read_short!(buffer)
|
58
|
+
raise DecodingError, "Need two bytes to decode a short, only #{buffer.size} bytes given" if buffer.size < 2
|
59
|
+
buffer.slice!(0, 2).unpack(Formats::SHORT_FORMAT).first
|
60
|
+
end
|
61
|
+
|
62
|
+
def read_string!(buffer)
|
63
|
+
length = read_short!(buffer)
|
64
|
+
raise DecodingError, "String length is #{length}, but only #{buffer.size} bytes given" if buffer.size < length
|
65
|
+
string = buffer.slice!(0, length)
|
66
|
+
string.force_encoding(::Encoding::UTF_8)
|
67
|
+
string
|
68
|
+
end
|
69
|
+
|
70
|
+
def read_long_string!(buffer)
|
71
|
+
length = read_int!(buffer)
|
72
|
+
raise DecodingError, "String length is #{length}, but only #{buffer.size} bytes given" if buffer.size < length
|
73
|
+
string = buffer.slice!(0, length)
|
74
|
+
string.force_encoding(::Encoding::UTF_8)
|
75
|
+
string
|
76
|
+
end
|
77
|
+
|
78
|
+
def read_uuid!(buffer)
|
79
|
+
raise DecodingError, "UUID requires 16 bytes, but only #{buffer.size} bytes given" if buffer.size < 16
|
80
|
+
Uuid.new(read_varint!(buffer, 16, false))
|
81
|
+
end
|
82
|
+
|
83
|
+
def read_string_list!(buffer)
|
84
|
+
size = read_short!(buffer)
|
85
|
+
size.times.map do
|
86
|
+
read_string!(buffer)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def read_bytes!(buffer)
|
91
|
+
size = read_int!(buffer)
|
92
|
+
return nil if size & 0x80000000 == 0x80000000
|
93
|
+
raise DecodingError, "Byte array length is #{size}, but only #{buffer.size} bytes given" if buffer.size < size
|
94
|
+
bytes = buffer.slice!(0, size)
|
95
|
+
bytes.force_encoding(::Encoding::BINARY)
|
96
|
+
bytes
|
97
|
+
end
|
98
|
+
|
99
|
+
def read_short_bytes!(buffer)
|
100
|
+
size = read_short!(buffer)
|
101
|
+
return nil if size & 0x8000 == 0x8000
|
102
|
+
raise DecodingError, "Byte array length is #{size}, but only #{buffer.size} bytes given" if buffer.size < size
|
103
|
+
bytes = buffer.slice!(0, size)
|
104
|
+
bytes.force_encoding(::Encoding::BINARY)
|
105
|
+
bytes
|
106
|
+
end
|
107
|
+
|
108
|
+
def read_option!(buffer)
|
109
|
+
id = read_short!(buffer)
|
110
|
+
value = nil
|
111
|
+
if block_given?
|
112
|
+
value = yield id, buffer
|
113
|
+
end
|
114
|
+
[id, value]
|
115
|
+
end
|
116
|
+
|
117
|
+
def read_inet!(buffer)
|
118
|
+
size = read_byte!(buffer)
|
119
|
+
raise DecodingError, "Inet requires #{size} bytes, but only #{buffer.size} bytes given" if buffer.size < size
|
120
|
+
ip_addr = IPAddr.new_ntoh(buffer.slice!(0, size))
|
121
|
+
port = read_int!(buffer)
|
122
|
+
[ip_addr, port]
|
123
|
+
end
|
124
|
+
|
125
|
+
def read_consistency!(buffer)
|
126
|
+
index = read_short!(buffer)
|
127
|
+
raise DecodingError, "Unknown consistency index #{index}" unless index < CONSISTENCIES.size
|
128
|
+
CONSISTENCIES[index]
|
129
|
+
end
|
130
|
+
|
131
|
+
def read_string_map!(buffer)
|
132
|
+
map = {}
|
133
|
+
map_size = read_short!(buffer)
|
134
|
+
map_size.times do
|
135
|
+
key = read_string!(buffer)
|
136
|
+
map[key] = read_string!(buffer)
|
137
|
+
end
|
138
|
+
map
|
139
|
+
end
|
140
|
+
|
141
|
+
def read_string_multimap!(buffer)
|
142
|
+
map = {}
|
143
|
+
map_size = read_short!(buffer)
|
144
|
+
map_size.times do
|
145
|
+
key = read_string!(buffer)
|
146
|
+
map[key] = read_string_list!(buffer)
|
147
|
+
end
|
148
|
+
map
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
DECIMAL_POINT = '.'.freeze
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|