neo4j-core 6.1.6 → 7.0.0.alpha.1

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