neo4j-core 6.1.6 → 7.0.0.alpha.1

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -9
  3. data/README.md +48 -0
  4. data/lib/neo4j-core.rb +23 -0
  5. data/lib/neo4j-core/helpers.rb +8 -0
  6. data/lib/neo4j-core/query.rb +23 -20
  7. data/lib/neo4j-core/query_clauses.rb +18 -32
  8. data/lib/neo4j-core/query_find_in_batches.rb +3 -1
  9. data/lib/neo4j-core/version.rb +1 -1
  10. data/lib/neo4j-embedded/cypher_response.rb +4 -0
  11. data/lib/neo4j-embedded/embedded_database.rb +3 -5
  12. data/lib/neo4j-embedded/embedded_node.rb +4 -4
  13. data/lib/neo4j-embedded/embedded_session.rb +21 -10
  14. data/lib/neo4j-embedded/embedded_transaction.rb +4 -10
  15. data/lib/neo4j-server/cypher_node.rb +5 -4
  16. data/lib/neo4j-server/cypher_relationship.rb +3 -3
  17. data/lib/neo4j-server/cypher_response.rb +4 -0
  18. data/lib/neo4j-server/cypher_session.rb +31 -22
  19. data/lib/neo4j-server/cypher_transaction.rb +23 -15
  20. data/lib/neo4j-server/resource.rb +3 -4
  21. data/lib/neo4j/core/cypher_session.rb +17 -9
  22. data/lib/neo4j/core/cypher_session/adaptors.rb +116 -33
  23. data/lib/neo4j/core/cypher_session/adaptors/bolt.rb +331 -0
  24. data/lib/neo4j/core/cypher_session/adaptors/bolt/chunk_writer_io.rb +76 -0
  25. data/lib/neo4j/core/cypher_session/adaptors/bolt/pack_stream.rb +288 -0
  26. data/lib/neo4j/core/cypher_session/adaptors/embedded.rb +60 -29
  27. data/lib/neo4j/core/cypher_session/adaptors/has_uri.rb +63 -0
  28. data/lib/neo4j/core/cypher_session/adaptors/http.rb +123 -119
  29. data/lib/neo4j/core/cypher_session/responses.rb +17 -2
  30. data/lib/neo4j/core/cypher_session/responses/bolt.rb +135 -0
  31. data/lib/neo4j/core/cypher_session/responses/embedded.rb +46 -11
  32. data/lib/neo4j/core/cypher_session/responses/http.rb +49 -40
  33. data/lib/neo4j/core/cypher_session/transactions.rb +33 -0
  34. data/lib/neo4j/core/cypher_session/transactions/bolt.rb +36 -0
  35. data/lib/neo4j/core/cypher_session/transactions/embedded.rb +32 -0
  36. data/lib/neo4j/core/cypher_session/transactions/http.rb +52 -0
  37. data/lib/neo4j/core/instrumentable.rb +2 -2
  38. data/lib/neo4j/core/label.rb +182 -0
  39. data/lib/neo4j/core/node.rb +8 -3
  40. data/lib/neo4j/core/relationship.rb +12 -4
  41. data/lib/neo4j/entity_equality.rb +1 -1
  42. data/lib/neo4j/session.rb +4 -5
  43. data/lib/neo4j/transaction.rb +108 -72
  44. data/neo4j-core.gemspec +6 -6
  45. metadata +34 -40
@@ -0,0 +1,331 @@
1
+ require 'neo4j/core/cypher_session/adaptors'
2
+ require 'neo4j/core/cypher_session/adaptors/has_uri'
3
+ require 'neo4j/core/cypher_session/adaptors/bolt/pack_stream'
4
+ require 'neo4j/core/cypher_session/adaptors/bolt/chunk_writer_io'
5
+ require 'neo4j/core/cypher_session/responses/bolt'
6
+ require 'io/wait'
7
+
8
+ # TODO: Work with `Query` objects
9
+ module Neo4j
10
+ module Core
11
+ class CypherSession
12
+ module Adaptors
13
+ class Bolt < Base
14
+ include Adaptors::HasUri
15
+ default_url('bolt://neo4:neo4j@localhost:7687')
16
+ validate_uri do |uri|
17
+ uri.scheme == 'bolt'
18
+ end
19
+
20
+ SUPPORTED_VERSIONS = [1, 0, 0, 0].freeze
21
+ VERSION = '0.0.1'.freeze
22
+
23
+ def initialize(url, options = {})
24
+ self.url = url
25
+ @options = options
26
+
27
+ open_socket
28
+ end
29
+
30
+ def connect
31
+ handshake
32
+
33
+ init
34
+
35
+ message = flush_messages[0]
36
+ return if message.type == :success
37
+
38
+ data = message.args[0]
39
+ fail "Init did not complete successfully\n\n#{data['code']}\n#{data['message']}"
40
+ end
41
+
42
+ def query_set(transaction, queries, options = {})
43
+ setup_queries!(queries, transaction, skip_instrumentation: options[:skip_instrumentation])
44
+
45
+ if @socket.ready?
46
+ debug_remaining_buffer
47
+ fail 'Making query, but expected there to be no buffer remaining!'
48
+ end
49
+
50
+ send_query_jobs(queries)
51
+
52
+ build_response(queries, options[:wrap_level] || @options[:wrap_level])
53
+ end
54
+
55
+ def version
56
+ fail 'should be implemented!'
57
+ end
58
+
59
+ def connected?
60
+ !!@socket
61
+ end
62
+
63
+ def indexes(session)
64
+ result = query(session, 'CALL db.indexes()', {}, skip_instrumentation: true)
65
+
66
+ result.map do |row|
67
+ label, property = row.description.match(/INDEX ON :([^\(]+)\(([^\)]+)\)/)[1, 2]
68
+ {type: row.type.to_sym, label: label.to_sym, properties: [property.to_sym]}
69
+ end
70
+ end
71
+
72
+ def constraints(session)
73
+ result = query(session, 'CALL db.indexes()', {}, skip_instrumentation: true)
74
+
75
+ result.select { |row| row.type == 'node_unique_property' }.map do |row|
76
+ label, property = row.description.match(/INDEX ON :([^\(]+)\(([^\)]+)\)/)[1, 2]
77
+ {type: :uniqueness, label: label.to_sym, properties: [property.to_sym]}
78
+ end
79
+ end
80
+
81
+ def self.transaction_class
82
+ require 'neo4j/core/cypher_session/transactions/bolt'
83
+ Neo4j::Core::CypherSession::Transactions::Bolt
84
+ end
85
+
86
+ instrument(:request, 'neo4j.core.bolt.request', %w(url body)) do |_, start, finish, _id, payload|
87
+ ms = (finish - start) * 1000
88
+
89
+ " #{ANSI::BLUE}BOLT REQUEST:#{ANSI::CLEAR} #{ANSI::YELLOW}#{ms.round}ms#{ANSI::CLEAR} #{payload[:url]}"
90
+ end
91
+
92
+ private
93
+
94
+ def build_response(queries, wrap_level)
95
+ catch(:cypher_bolt_failure) do
96
+ Responses::Bolt.new(queries, method(:flush_messages), wrap_level: wrap_level).results
97
+ end.tap do |error_data|
98
+ handle_failure!(error_data) if !error_data.is_a?(Array)
99
+ end
100
+ end
101
+
102
+ def handle_failure!(error_data)
103
+ flush_messages
104
+
105
+ send_job do |job|
106
+ job.add_message(:ack_failure)
107
+ end
108
+ fail 'Expected SUCCESS for ACK_FAILURE' if flush_messages[0].type != :success
109
+
110
+ fail CypherError.new_from(error_data['code'], error_data['message'])
111
+ end
112
+
113
+ def debug_remaining_buffer
114
+ logger.debug 'Remaining buffer:'
115
+
116
+ i = 0
117
+ while @socket.ready?
118
+ i += 1
119
+ logger.debug "Message set #{i}:"
120
+ flush_messages
121
+ end
122
+ end
123
+
124
+ def send_query_jobs(queries)
125
+ send_job do |job|
126
+ queries.each do |query|
127
+ job.add_message(:run, query.cypher, query.parameters || {})
128
+ job.add_message(:pull_all)
129
+ end
130
+ end
131
+ end
132
+
133
+ def new_job
134
+ Job.new(self)
135
+ end
136
+
137
+ def open_socket
138
+ @socket = TCPSocket.open(host, port)
139
+ end
140
+
141
+ GOGOBOLT = "\x60\x60\xB0\x17"
142
+ def handshake
143
+ log_message :C, :handshake, nil
144
+
145
+ sendmsg(GOGOBOLT + SUPPORTED_VERSIONS.pack('l>*'))
146
+
147
+ agreed_version = recvmsg(4).unpack('l>*')[0]
148
+
149
+ if agreed_version.zero?
150
+ @socket.shutdown(Socket::SHUT_RDWR)
151
+ @socket.close
152
+
153
+ fail "Couldn't agree on a version (Sent versions #{SUPPORTED_VERSIONS.inspect})"
154
+ end
155
+
156
+ logger.debug "Agreed to version: #{agreed_version}"
157
+ end
158
+
159
+ def init
160
+ send_job do |job|
161
+ job.add_message(:init, USER_AGENT_STRING, principal: user, credentials: password, scheme: 'basic')
162
+ end
163
+ end
164
+
165
+ # Allows you to send messages to the server
166
+ # Returns an array of Message objects
167
+ def send_job
168
+ new_job.tap do |job|
169
+ yield job
170
+ log_message :C, :job, job
171
+ sendmsg(job.chunked_packed_stream)
172
+ end
173
+ end
174
+
175
+ STREAM_INSPECTOR = lambda do |stream|
176
+ stream.bytes.map { |byte| byte.to_s(16).rjust(2, '0') }.join(':')
177
+ end
178
+
179
+ def sendmsg(message)
180
+ log_message :C, message
181
+
182
+ @socket.sendmsg(message)
183
+ end
184
+
185
+ def recvmsg(size, timeout = 10)
186
+ Timeout.timeout(timeout) do
187
+ @socket.recv(size).tap do |result|
188
+ log_message :S, result
189
+ end
190
+ end
191
+ rescue Timeout::Error
192
+ raise "Timed out waiting for #{size} bytes from Neo4j (after #{timeout} seconds)"
193
+ end
194
+
195
+ def flush_messages
196
+ if structures = flush_response
197
+ structures.map do |structure|
198
+ Message.new(structure.signature, *structure.list).tap do |message|
199
+ log_message :S, message.type, message.args.join(' ')
200
+ end
201
+ end.tap { flush_response }
202
+ end
203
+ end
204
+
205
+ def log_message(side, *args)
206
+ if args.size == 1
207
+ logger.debug "#{side}: #{STREAM_INSPECTOR.call(args[0])}"
208
+ else
209
+ type, message = args
210
+ logger.debug "#{side}: #{ANSI::CYAN}#{type.to_s.upcase}#{ANSI::CLEAR} #{message}"
211
+ end
212
+ end
213
+
214
+ # Replace with Enumerator?
215
+ def flush_response
216
+ if !(header = recvmsg(2)).empty? && (chunk_size = header.unpack('s>*')[0]) > 0
217
+ log_message :S, :chunk_size, chunk_size
218
+
219
+ chunk = recvmsg(chunk_size)
220
+
221
+ unpacker = PackStream::Unpacker.new(StringIO.new(chunk))
222
+
223
+ [].tap { |r| while arg = unpacker.unpack_value!; r << arg; end }
224
+ end
225
+ end
226
+
227
+ # Represents messages sent to or received from the server
228
+ class Message
229
+ TYPE_CODES = {
230
+ # client message types
231
+ init: 0x01, # 0000 0001 // INIT <user_agent>
232
+ ack_failure: 0x0E, # 0000 1110 // ACK_FAILURE
233
+ reset: 0x0F, # 0000 1111 // RESET
234
+ run: 0x10, # 0001 0000 // RUN <statement> <parameters>
235
+ discard_all: 0x2F, # 0010 1111 // DISCARD *
236
+ pull_all: 0x3F, # 0011 1111 // PULL *
237
+
238
+ # server message types
239
+ success: 0x70, # 0111 0000 // SUCCESS <metadata>
240
+ record: 0x71, # 0111 0001 // RECORD <value>
241
+ ignored: 0x7E, # 0111 1110 // IGNORED <metadata>
242
+ failure: 0x7F # 0111 1111 // FAILURE <metadata>
243
+ }.freeze
244
+
245
+ CODE_TYPES = TYPE_CODES.invert
246
+
247
+ def initialize(type_or_code, *args)
248
+ @type_code = Message.type_code_for(type_or_code)
249
+ fail "Invalid message type: #{@type_code.inspect}" if !@type_code
250
+ @type = CODE_TYPES[@type_code]
251
+
252
+ @args = args
253
+ end
254
+
255
+ def struct
256
+ PackStream::Structure.new(@type_code, @args)
257
+ end
258
+
259
+ def to_s
260
+ "#{ANSI::GREEN}#{@type.to_s.upcase}#{ANSI::CLEAR} #{@args.inspect if !@args.empty?}"
261
+ end
262
+
263
+ def packed_stream
264
+ PackStream::Packer.new(struct).packed_stream
265
+ end
266
+
267
+ def value
268
+ return if @type != :record
269
+ @args.map do |arg|
270
+ # Assuming result is Record
271
+ field_names = arg[1]
272
+
273
+ field_values = arg[2].map do |field_value|
274
+ Message.interpret_field_value(field_value)
275
+ end
276
+
277
+ Hash[field_names.zip(field_values)]
278
+ end
279
+ end
280
+
281
+ attr_reader :type, :args
282
+
283
+ def self.type_code_for(type_or_code)
284
+ type_or_code.is_a?(Integer) ? type_or_code : TYPE_CODES[type_or_code]
285
+ end
286
+
287
+ def self.interpret_field_value(value)
288
+ if value.is_a?(Array) && (1..3).cover?(value[0])
289
+ case value[0]
290
+ when 1
291
+ {type: :node, identity: value[1],
292
+ labels: value[2], properties: value[3]}
293
+ end
294
+ else
295
+ value
296
+ end
297
+ end
298
+ end
299
+
300
+ # Represents a set of messages to send to the server
301
+ class Job
302
+ def initialize(session)
303
+ @messages = []
304
+ @session = session
305
+ end
306
+
307
+ def add_message(type, *args)
308
+ @messages << Message.new(type, *args)
309
+ end
310
+
311
+ def chunked_packed_stream
312
+ io = ChunkWriterIO.new
313
+
314
+ @messages.each do |message|
315
+ io.write(message.packed_stream)
316
+ io.flush(true)
317
+ end
318
+
319
+ io.rewind
320
+ io.read
321
+ end
322
+
323
+ def to_s
324
+ @messages.join(' | ')
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,76 @@
1
+ require 'stringio'
2
+
3
+ class ChunkWriterIO < StringIO
4
+ # Writer for chunked data.
5
+
6
+ MAX_CHUNK_SIZE = 0xFFFF
7
+
8
+ def initialize
9
+ @output_buffer = []
10
+ @output_size = 0
11
+ super
12
+ end
13
+
14
+ # Write some bytes, splitting into chunks if necessary.
15
+ def write_with_chunking(string)
16
+ # Kernel.puts "Write!"
17
+ until string.empty?
18
+ future_size = @output_size + string.size
19
+ if future_size >= MAX_CHUNK_SIZE
20
+ last = MAX_CHUNK_SIZE - @output_size
21
+ write_buffer!(string[0, last], MAX_CHUNK_SIZE)
22
+ string = string[last..-1]
23
+
24
+ write_without_chunking(buffer_result)
25
+ clear_buffer!
26
+ else
27
+ write_buffer!(string, future_size)
28
+
29
+ string = ''
30
+ end
31
+ end
32
+ end
33
+
34
+ alias write_without_chunking write
35
+ alias write write_with_chunking
36
+
37
+ def flush(zero_chunk = false)
38
+ write_without_chunking(buffer_result(zero_chunk))
39
+ clear_buffer!
40
+
41
+ super()
42
+ end
43
+
44
+ # Close the stream.
45
+ def close(zero_chunk = false)
46
+ flush(zero_chunk)
47
+ super
48
+ end
49
+
50
+ # private
51
+ def write_buffer!(string, size)
52
+ @output_buffer << string
53
+ @output_size = size
54
+ end
55
+
56
+ def buffer_result(zero_chunk = false)
57
+ result = ''
58
+
59
+ # Kernel.puts 'result1', result.inspect
60
+ if !@output_buffer.empty?
61
+ result << [@output_size].pack('s>*')
62
+ result.concat(@output_buffer.join)
63
+ end
64
+
65
+ # Kernel.puts 'result2', result.inspect
66
+ result << "\x00\x00" if zero_chunk
67
+ # Kernel.puts 'result3', result.inspect
68
+
69
+ result
70
+ end
71
+
72
+ def clear_buffer!
73
+ @output_buffer.clear
74
+ @output_size = 0
75
+ end
76
+ end
@@ -0,0 +1,288 @@
1
+ require 'stringio'
2
+
3
+ module Neo4j
4
+ module Core
5
+ # Implements the the PackStream packing and unpacking specifications
6
+ # as specified by Neo Technology for the Neo4j graph database
7
+ module PackStream
8
+ MARKER_TYPES = {
9
+ C0: nil,
10
+ C1: [:float, 64],
11
+ C2: false,
12
+ C3: true,
13
+ C8: [:int, 8],
14
+ C9: [:int, 16],
15
+ CA: [:int, 32],
16
+ CB: [:int, 64],
17
+ CC: [:bytes, 8],
18
+ CD: [:bytes, 16],
19
+ CE: [:bytes, 32],
20
+ D0: [:text, 8],
21
+ D1: [:text, 16],
22
+ D2: [:text, 32],
23
+ D4: [:list, 8],
24
+ D5: [:list, 16],
25
+ D6: [:list, 32],
26
+ D8: [:map, 8],
27
+ D9: [:map, 16],
28
+ DA: [:map, 32],
29
+ DC: [:struct, 8],
30
+ DD: [:struct, 16],
31
+ DE: [:struct, 32]
32
+ }
33
+ # For efficiency. Translates directly from bytes to types
34
+ MARKER_TYPES.keys.each do |key|
35
+ ord = eval("0x#{key}") # rubocop:disable Lint/Eval
36
+ MARKER_TYPES[ord] = MARKER_TYPES.delete(key)
37
+ end
38
+
39
+ # Translates directly from types to bytes
40
+ MARKER_BYTES = MARKER_TYPES.invert
41
+ MARKER_BYTES.keys.each do |key|
42
+ MARKER_BYTES.delete(key) if key.is_a?(Array)
43
+ end
44
+
45
+ MARKER_HEADERS = MARKER_TYPES.each_with_object({}) do |(byte, (type, size)), headers|
46
+ headers[type] ||= {}
47
+ headers[type][size] = [byte].pack('C')
48
+ end
49
+
50
+ HEADER_PACK_STRINGS = %w(C S L).freeze
51
+
52
+ Structure = Struct.new(:signature, :list)
53
+
54
+ # Object which holds a Ruby object and can
55
+ # pack it into a PackStream stream
56
+ class Packer
57
+ def initialize(object)
58
+ @object = object
59
+ end
60
+
61
+ def packed_stream
62
+ if byte = MARKER_BYTES[@object]
63
+ pack_array_as_string([byte])
64
+ else
65
+ case @object
66
+ when Date, Time, DateTime then string_stream
67
+ when Integer, Float, String, Symbol, Array, Structure, Hash
68
+ send(@object.class.name.split('::').last.downcase + '_stream')
69
+ end
70
+ end
71
+ end
72
+
73
+ # Range Minimum | Range Maximum | Representation | Byte |
74
+ # ============================|============================|================|======|
75
+ # -9 223 372 036 854 775 808 | -2 147 483 649 | INT_64 | CB |
76
+ # -2 147 483 648 | -32 769 | INT_32 | CA |
77
+ # -32 768 | -129 | INT_16 | C9 |
78
+ # -128 | -17 | INT_8 | C8 |
79
+ # -16 | +127 | TINY_INT | N/A |
80
+ # +128 | +32 767 | INT_16 | C9 |
81
+ # +32 768 | +2 147 483 647 | INT_32 | CA |
82
+ # +2 147 483 648 | +9 223 372 036 854 775 807 | INT_64 | CB |
83
+
84
+ INT_HEADERS = MARKER_HEADERS[:int]
85
+ def integer_stream
86
+ case @object
87
+ when -0x10...0x80 # TINY_INT
88
+ pack_integer_object_as_string
89
+ when -0x80...-0x10 # INT_8
90
+ INT_HEADERS[8] + pack_integer_object_as_string
91
+ when -0x8000...0x8000 # INT_16
92
+ INT_HEADERS[16] + pack_integer_object_as_string(2)
93
+ when -0x80000000...0x80000000 # INT_32
94
+ INT_HEADERS[32] + pack_integer_object_as_string(4)
95
+ when -0x8000000000000000...0x8000000000000000 # INT_64
96
+ INT_HEADERS[64] + pack_integer_object_as_string(8)
97
+ end
98
+ end
99
+
100
+ alias fixnum_stream integer_stream
101
+ alias bignum_stream integer_stream
102
+
103
+ def float_stream
104
+ MARKER_HEADERS[:float][64] + [@object].pack('G').force_encoding(Encoding::BINARY)
105
+ end
106
+
107
+ # Marker | Size | Maximum size
108
+ # ========|=============================================|=====================
109
+ # 80..8F | contained within low-order nibble of marker | 15 bytes
110
+ # D0 | 8-bit big-endian unsigned integer | 255 bytes
111
+ # D1 | 16-bit big-endian unsigned integer | 65 535 bytes
112
+ # D2 | 32-bit big-endian unsigned integer | 4 294 967 295 bytes
113
+
114
+ def string_stream
115
+ s = @object.to_s
116
+ s = s.dup if s.frozen?
117
+ marker_string(0x80, 0xD0, @object.to_s.bytesize) + s.force_encoding(Encoding::BINARY)
118
+ end
119
+
120
+ alias symbol_stream string_stream
121
+
122
+ def array_stream
123
+ marker_string(0x90, 0xD4, @object.size) + @object.map do |e|
124
+ Packer.new(e).packed_stream
125
+ end.join
126
+ end
127
+
128
+ def structure_stream
129
+ fail 'Structure too big' if @object.list.size > 65_535
130
+ marker_string(0xB0, 0xDC, @object.list.size) + [@object.signature].pack('C') + @object.list.map do |e|
131
+ Packer.new(e).packed_stream
132
+ end.join
133
+ end
134
+
135
+ def hash_stream
136
+ marker_string(0xA0, 0xD8, @object.size) +
137
+ @object.map do |key, value|
138
+ Packer.new(key).packed_stream +
139
+ Packer.new(value).packed_stream
140
+ end.join
141
+ end
142
+
143
+ def self.pack_arguments(*objects)
144
+ objects.map { |o| new(o).packed_stream }.join
145
+ end
146
+
147
+ private
148
+
149
+ def marker_string(tiny_base, regular_base, size)
150
+ head_byte = case size
151
+ when 0...0x10 then tiny_base + size
152
+ when 0x10...0x100 then regular_base
153
+ when 0x100...0x10000 then regular_base + 1
154
+ when 0x10000...0x100000000 then regular_base + 2
155
+ end
156
+
157
+ result = [head_byte].pack('C')
158
+ result += [size].pack(HEADER_PACK_STRINGS[head_byte - regular_base]).reverse if size >= 0x10
159
+ result
160
+ end
161
+
162
+ def pack_integer_object_as_string(size = 1)
163
+ bytes = []
164
+ (0...size).to_a.reverse.inject(@object) do |current, i|
165
+ bytes << (current / (256**i))
166
+ current % (256**i)
167
+ end
168
+
169
+ pack_array_as_string(bytes)
170
+ end
171
+
172
+ def pack_array_as_string(a)
173
+ a.pack('c*')
174
+ end
175
+ end
176
+
177
+ # Object which holds a stream of PackStream data
178
+ # and can unpack it
179
+ class Unpacker
180
+ def initialize(stream)
181
+ @stream = stream
182
+ end
183
+
184
+ HEADER_BASE_BYTES = {text: 0xD0, list: 0xD4, struct: 0xDC, map: 0xD8}.freeze
185
+
186
+ def unpack_value!
187
+ return nil if depleted?
188
+
189
+ marker = shift_byte!
190
+
191
+ if type_and_size = PackStream.marker_type_and_size(marker)
192
+ type, size = type_and_size
193
+
194
+ shift_value_for_type!(type, size, marker)
195
+ elsif MARKER_TYPES.key?(marker)
196
+ MARKER_TYPES[marker]
197
+ else
198
+ marker >= 0xF0 ? -0x100 + marker : marker
199
+ end
200
+ end
201
+
202
+ private
203
+
204
+ METHOD_MAP = {
205
+ int: :value_for_int!,
206
+ float: :value_for_float!,
207
+ tiny_list: :value_for_list!,
208
+ list: :value_for_list!,
209
+ tiny_map: :value_for_map!,
210
+ map: :value_for_map!,
211
+ tiny_struct: :value_for_struct!,
212
+ struct: :value_for_struct!
213
+ }
214
+
215
+ def shift_value_for_type!(type, size, marker)
216
+ if [:text, :list, :map, :struct].include?(type)
217
+ offset = marker - HEADER_BASE_BYTES[type]
218
+ size = shift_stream!(2 << (offset - 1)).reverse.unpack(HEADER_PACK_STRINGS[offset])[0]
219
+ end
220
+
221
+ if [:tiny_text, :text, :bytes].include?(type)
222
+ shift_stream!(size).force_encoding('UTF-8')
223
+ else
224
+ send(METHOD_MAP[type], size)
225
+ end
226
+ end
227
+
228
+ def value_for_int!(size)
229
+ r = shift_bytes!(size >> 3).reverse.each_with_index.inject(0) do |sum, (byte, i)|
230
+ sum + (byte * (256**i))
231
+ end
232
+
233
+ (r >> (size - 1)) == 1 ? (r - (2**size)) : r
234
+ end
235
+
236
+ def value_for_float!(_size)
237
+ shift_stream!(8).unpack('G')[0]
238
+ end
239
+
240
+ def value_for_map!(size)
241
+ size.times.each_with_object({}) do |_, r|
242
+ key = unpack_value!
243
+ r[key] = unpack_value!
244
+ end
245
+ end
246
+
247
+ def value_for_list!(size)
248
+ Array.new(size) { unpack_value! }
249
+ end
250
+
251
+ def value_for_struct!(size)
252
+ Structure.new(shift_byte!, value_for_list!(size))
253
+ end
254
+
255
+
256
+ def shift_byte!
257
+ shift_bytes!(1).first unless depleted?
258
+ end
259
+
260
+ def shift_bytes!(length)
261
+ result = shift_stream!(length)
262
+ result && result.bytes.to_a
263
+ end
264
+
265
+ def shift_stream!(length)
266
+ @stream.read(length) if !depleted? || length.zero?
267
+ end
268
+
269
+ def depleted?
270
+ @stream.eof?
271
+ end
272
+ end
273
+
274
+ def self.marker_type_and_size(marker)
275
+ if (marker_spec = MARKER_TYPES[marker]).is_a?(Array)
276
+ marker_spec
277
+ else
278
+ case marker
279
+ when 0x80..0x8F then [:tiny_text, marker - 0x80]
280
+ when 0x90..0x9F then [:tiny_list, marker - 0x90]
281
+ when 0xA0..0xAF then [:tiny_map, marker - 0xA0]
282
+ when 0xB0..0xBF then [:tiny_struct, marker - 0xB0]
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end