herringbone 0.2.0 → 0.3.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.
@@ -5,30 +5,52 @@ module Herringbone
5
5
  # Structs are described declaratively (see Herringbone::Thrift::Struct) so that
6
6
  # both the reader and the writer are driven by the same field tables.
7
7
  module Thrift
8
+ # Raised on malformed or truncated Thrift data, and on values the writer cannot encode
8
9
  class Error < StandardError; end
9
10
 
10
- # Compact protocol wire types
11
+ # Compact protocol wire type: end of a struct's fields
11
12
  T_STOP = 0
13
+ # Compact protocol wire type: boolean true (the value lives in the field header)
12
14
  T_TRUE = 1
15
+ # Compact protocol wire type: boolean false (the value lives in the field header)
13
16
  T_FALSE = 2
17
+ # Compact protocol wire type: signed 8-bit integer, one raw byte
14
18
  T_BYTE = 3
19
+ # Compact protocol wire type: 16-bit integer, zigzag varint
15
20
  T_I16 = 4
21
+ # Compact protocol wire type: 32-bit integer, zigzag varint
16
22
  T_I32 = 5
23
+ # Compact protocol wire type: 64-bit integer, zigzag varint
17
24
  T_I64 = 6
25
+ # Compact protocol wire type: little-endian IEEE 754 double, 8 bytes
18
26
  T_DOUBLE = 7
27
+ # Compact protocol wire type: varint length followed by raw bytes
19
28
  T_BINARY = 8
29
+ # Compact protocol wire type: list (size and element type header, then the elements)
20
30
  T_LIST = 9
31
+ # Compact protocol wire type: set (encoded like a list)
21
32
  T_SET = 10
33
+ # Compact protocol wire type: map (only skipped, Parquet metadata has none)
22
34
  T_MAP = 11
35
+ # Compact protocol wire type: nested struct, terminated by T_STOP
23
36
  T_STRUCT = 12
24
37
 
25
38
  # Declared field types used in struct definitions
26
39
  # :bool, :byte, :i16, :i32, :i64, :double, :binary, :string, [:list, elem], StructClass
40
+ #
41
+ # Maps the scalar declared types to the wire type written for them. +:bool+ maps to T_TRUE,
42
+ # but the writer picks T_TRUE or T_FALSE from the value.
27
43
  WIRE_TYPES = {
28
44
  bool: T_TRUE, byte: T_BYTE, i16: T_I16, i32: T_I32, i64: T_I64,
29
45
  double: T_DOUBLE, binary: T_BINARY, string: T_BINARY
30
46
  }.freeze
31
47
 
48
+ # Wire type written for a declared field type
49
+ #
50
+ # @param type [Symbol, Array, Class] a scalar type from WIRE_TYPES, a +[:list, elem]+ Array,
51
+ # or a Struct subclass
52
+ # @return [Integer] one of the T_* wire type constants (T_TRUE for +:bool+)
53
+ # @raise [KeyError] for an unknown scalar type Symbol
32
54
  def self.wire_type_for(type)
33
55
  case type
34
56
  when Symbol then WIRE_TYPES.fetch(type)
@@ -38,6 +60,13 @@ module Herringbone
38
60
  end
39
61
 
40
62
  # Whether a value of wire type +wire+ can be read as declared +type+
63
+ #
64
+ # Integer widths are interchangeable (any of i16/i32/i64 on the wire is read for any declared
65
+ # integer type), and a set is accepted where a list is declared.
66
+ #
67
+ # @param wire [Integer] wire type from the field or list header
68
+ # @param type [Symbol, Array, Class] declared field type (see WIRE_TYPES)
69
+ # @return [Boolean] true when the value can be decoded as +type+, false when it must be skipped
41
70
  def self.compatible?(wire, type)
42
71
  case type
43
72
  when :bool then wire == T_TRUE || wire == T_FALSE
@@ -50,14 +79,22 @@ module Herringbone
50
79
  end
51
80
  end
52
81
 
82
+ # Decodes compact protocol data from a String, keeping a byte position into it
53
83
  class Reader
84
+ # @return [Integer] byte offset of the next byte to read
54
85
  attr_reader :pos
55
86
 
87
+ # @param buf [String] the encoded data (binary)
88
+ # @param pos [Integer] byte offset to start reading at
56
89
  def initialize(buf, pos = 0)
57
90
  @buf = buf
58
91
  @pos = pos
59
92
  end
60
93
 
94
+ # Reads one raw byte
95
+ #
96
+ # @return [Integer] the byte, 0..255
97
+ # @raise [Error] at the end of the buffer
61
98
  def read_byte
62
99
  b = @buf.getbyte(@pos)
63
100
  raise Error, "Unexpected end of Thrift data at #{@pos}" unless b
@@ -65,6 +102,10 @@ module Herringbone
65
102
  b
66
103
  end
67
104
 
105
+ # Reads an unsigned LEB128 varint
106
+ #
107
+ # @return [Integer] the decoded non-negative value
108
+ # @raise [Error] when the varint runs past the buffer or is longer than 64 bits allow
68
109
  def read_varint
69
110
  result = 0
70
111
  shift = 0
@@ -77,11 +118,19 @@ module Herringbone
77
118
  end
78
119
  end
79
120
 
121
+ # Reads a zigzag-encoded varint (how i16, i32, i64 and field id deltas are stored)
122
+ #
123
+ # @return [Integer] the decoded signed value
124
+ # @raise [Error] when the varint runs past the buffer
80
125
  def read_zigzag
81
126
  n = read_varint
82
127
  (n >> 1) ^ -(n & 1)
83
128
  end
84
129
 
130
+ # Reads a length-prefixed byte string
131
+ #
132
+ # @return [String] a slice of the buffer (with the buffer's encoding)
133
+ # @raise [Error] when the length runs past the end of the buffer
85
134
  def read_binary
86
135
  len = read_varint
87
136
  raise Error, "Binary length #{len} exceeds buffer" if @pos + len > @buf.bytesize
@@ -90,6 +139,9 @@ module Herringbone
90
139
  s
91
140
  end
92
141
 
142
+ # Reads an 8-byte little-endian double
143
+ #
144
+ # @return [Float, nil] the value (nil when fewer than 8 bytes are left: this is not checked)
93
145
  def read_double
94
146
  v = @buf.byteslice(@pos, 8).unpack1("E")
95
147
  @pos += 8
@@ -97,6 +149,13 @@ module Herringbone
97
149
  end
98
150
 
99
151
  # Reads a struct of the given class, returning an instance.
152
+ #
153
+ # Fields the class does not declare, or whose wire type does not match the declared type,
154
+ # are skipped, so newer writers' additions do not break reading.
155
+ #
156
+ # @param klass [Class] a Thrift::Struct subclass
157
+ # @return [Struct] an instance of +klass+ with the fields that were present set
158
+ # @raise [Error] on truncated or malformed data
100
159
  def read_struct(klass)
101
160
  obj = klass.new
102
161
  fields = klass.fields_by_id
@@ -118,18 +177,26 @@ module Herringbone
118
177
  obj
119
178
  end
120
179
 
180
+ # Reads one value of wire type +wire+ as declared +type+
181
+ #
182
+ # @param wire [Integer] wire type from the field or list header
183
+ # @param type [Symbol, Array, Class] declared type; +:string+ makes binary values UTF-8,
184
+ # an Array or Struct subclass gives the element type or struct to read
185
+ # @return [Object] true/false, an Integer (bytes are signed), a Float, a String, an Array
186
+ # (or nil, see #read_list) or a Struct
187
+ # @raise [Error] on an unsupported wire type or truncated data
121
188
  def read_value(wire, type)
122
189
  case wire
123
190
  when T_TRUE then true
124
191
  when T_FALSE then false
125
192
  when T_BYTE
126
193
  b = read_byte
127
- b >= 0x80 ? b - 0x100 : b
194
+ (b >= 0x80) ? b - 0x100 : b
128
195
  when T_I16, T_I32, T_I64 then read_zigzag
129
196
  when T_DOUBLE then read_double
130
197
  when T_BINARY
131
198
  s = read_binary
132
- type == :string ? s.force_encoding(Encoding::UTF_8) : s
199
+ (type == :string) ? s.force_encoding(Encoding::UTF_8) : s
133
200
  when T_LIST, T_SET then read_list(type)
134
201
  when T_STRUCT then read_struct(type)
135
202
  else
@@ -137,6 +204,13 @@ module Herringbone
137
204
  end
138
205
  end
139
206
 
207
+ # Reads a list or set, whose header holds the size (or 15 and a varint size) and the
208
+ # element wire type
209
+ #
210
+ # @param type [Array] declared list type, +[:list, elem_type]+
211
+ # @return [Array, nil] the elements, or nil (with the list skipped) when the element wire
212
+ # type does not match +elem_type+
213
+ # @raise [Error] on truncated or malformed data
140
214
  def read_list(type)
141
215
  header = read_byte
142
216
  size = header >> 4
@@ -144,7 +218,7 @@ module Herringbone
144
218
  elem_wire = header & 0x0F
145
219
  elem_type = type[1]
146
220
  bool_elems = elem_wire == T_TRUE || elem_wire == T_FALSE
147
- unless size.zero? || (bool_elems && elem_type == :bool) || Thrift.compatible?(elem_wire, elem_type)
221
+ if size.positive? && !(bool_elems && elem_type == :bool) && !Thrift.compatible?(elem_wire, elem_type)
148
222
  size.times { bool_elems ? read_byte : skip(elem_wire) }
149
223
  return nil
150
224
  end
@@ -158,6 +232,11 @@ module Herringbone
158
232
  end
159
233
  end
160
234
 
235
+ # Advances past a value of wire type +wire+ without decoding it
236
+ #
237
+ # @param wire [Integer] wire type of the value to skip
238
+ # @return [void]
239
+ # @raise [Error] on an unknown wire type (including T_STOP) or truncated data
161
240
  def skip(wire)
162
241
  case wire
163
242
  when T_TRUE, T_FALSE then nil
@@ -193,13 +272,21 @@ module Herringbone
193
272
  end
194
273
  end
195
274
 
275
+ # Encodes compact protocol data by appending to a binary String
196
276
  class Writer
277
+ # @return [String] the encoded data so far
197
278
  attr_reader :buf
198
279
 
280
+ # @param buf [String] binary String to append to
199
281
  def initialize(buf = String.new(capacity: 1024, encoding: Encoding::BINARY))
200
282
  @buf = buf
201
283
  end
202
284
 
285
+ # Writes an unsigned LEB128 varint
286
+ #
287
+ # @param n [Integer] non-negative value
288
+ # @return [void]
289
+ # @raise [Error] when +n+ is negative
203
290
  def write_varint(n)
204
291
  raise Error, "Negative varint" if n.negative?
205
292
  while n >= 0x80
@@ -209,15 +296,29 @@ module Herringbone
209
296
  @buf << n
210
297
  end
211
298
 
299
+ # Writes a signed integer as a zigzag varint
300
+ #
301
+ # @param n [Integer] signed value
302
+ # @return [void]
212
303
  def write_zigzag(n)
213
304
  write_varint(n.negative? ? ((-n) << 1) - 1 : n << 1)
214
305
  end
215
306
 
307
+ # Writes a length-prefixed byte string
308
+ #
309
+ # @param s [String] value whose bytes are written (in any encoding)
310
+ # @return [void]
216
311
  def write_binary(s)
217
312
  write_varint(s.bytesize)
218
313
  @buf << s.b
219
314
  end
220
315
 
316
+ # Writes the non-nil fields of a struct in field id order, then T_STOP. Field ids are
317
+ # written as a delta in the header when it is 1..15, else as a separate zigzag varint.
318
+ # Booleans are carried by the header's wire type alone.
319
+ #
320
+ # @param obj [Struct] instance of a Thrift::Struct subclass
321
+ # @return [void]
221
322
  def write_struct(obj)
222
323
  last_id = 0
223
324
  obj.class.fields.each do |field|
@@ -241,6 +342,12 @@ module Herringbone
241
342
  @buf << T_STOP
242
343
  end
243
344
 
345
+ # Writes one value of a declared type. Not for +:bool+, whose value goes in a field or
346
+ # list header (see #write_struct and #write_list).
347
+ #
348
+ # @param type [Symbol, Array, Class] declared type (see WIRE_TYPES)
349
+ # @param value [Object] Integer, Float, String, Array or Struct matching +type+
350
+ # @return [void]
244
351
  def write_value(type, value)
245
352
  case type
246
353
  when :byte then @buf << (value & 0xFF)
@@ -252,6 +359,12 @@ module Herringbone
252
359
  end
253
360
  end
254
361
 
362
+ # Writes a list: a header with the size (inline when below 15) and element wire type,
363
+ # then the elements. Booleans in lists are written as full bytes.
364
+ #
365
+ # @param elem_type [Symbol, Array, Class] declared element type
366
+ # @param values [Array] the elements
367
+ # @return [void]
255
368
  def write_list(elem_type, values)
256
369
  elem_wire = Thrift.wire_type_for(elem_type)
257
370
  if values.size < 15
@@ -270,20 +383,39 @@ module Herringbone
270
383
  end
271
384
  end
272
385
 
386
+ # A field declared on a Thrift::Struct subclass
387
+ #
388
+ # @!attribute [rw] id
389
+ # @return [Integer] Thrift field id
390
+ # @!attribute [rw] name
391
+ # @return [Symbol] accessor name
392
+ # @!attribute [rw] type
393
+ # @return [Symbol, Array, Class] declared type (see WIRE_TYPES)
394
+ # @!attribute [rw] ivar
395
+ # @return [Symbol] instance variable holding the value (+:@name+)
273
396
  Field = ::Struct.new(:id, :name, :type, :ivar)
274
397
 
275
398
  # Base class for Thrift structs. Subclasses declare fields with
276
399
  # field 1, :name, :i32
277
400
  class Struct
278
401
  class << self
402
+ # @return [Array<Field>] declared fields, sorted by id
279
403
  def fields
280
404
  @fields ||= []
281
405
  end
282
406
 
407
+ # @return [Hash{Integer => Field}] declared fields by id, for decoding
283
408
  def fields_by_id
284
409
  @fields_by_id ||= fields.to_h { |f| [f.id, f] }
285
410
  end
286
411
 
412
+ # Declares a field and defines its accessor
413
+ #
414
+ # @param id [Integer] Thrift field id from parquet.thrift
415
+ # @param name [Symbol] accessor name
416
+ # @param type [Symbol, Array, Class] declared type: a WIRE_TYPES key, +[:list, elem]+
417
+ # or a Struct subclass
418
+ # @return [void]
287
419
  def field(id, name, type)
288
420
  fields << Field.new(id, name, type, :"@#{name}")
289
421
  fields.sort_by!(&:id)
@@ -291,12 +423,22 @@ module Herringbone
291
423
  attr_accessor name
292
424
  end
293
425
 
426
+ # Decodes an instance from +buf+ starting at +pos+
427
+ #
428
+ # @param buf [String] encoded data
429
+ # @param pos [Integer] byte offset of the struct in +buf+
430
+ # @return [Array(Struct, Integer)] the instance and the offset just past it
431
+ # @raise [Error] on truncated or malformed data
294
432
  def decode(buf, pos = 0)
295
433
  reader = Reader.new(buf, pos)
296
434
  [reader.read_struct(self), reader.pos]
297
435
  end
298
436
  end
299
437
 
438
+ # @param attrs [Hash{Symbol => Object}] initial field values, by accessor name
439
+ # @option attrs [Object] :any_declared_field value for that field (nil leaves it unset); the
440
+ # keys are whatever fields the subclass declares
441
+ # @raise [ArgumentError] for a name that is not a declared field
300
442
  def initialize(**attrs)
301
443
  attrs.each do |k, v|
302
444
  raise ArgumentError, "Unknown field #{k} for #{self.class}" unless respond_to?(:"#{k}=")
@@ -304,12 +446,16 @@ module Herringbone
304
446
  end
305
447
  end
306
448
 
449
+ # @return [String] the compact protocol encoding (binary)
307
450
  def encode
308
451
  w = Writer.new
309
452
  w.write_struct(self)
310
453
  w.buf
311
454
  end
312
455
 
456
+ # Set fields as a Hash, with nested structs (also inside lists) converted too
457
+ #
458
+ # @return [Hash{Symbol => Object}] values by field name, nil fields left out
313
459
  def to_h
314
460
  self.class.fields.each_with_object({}) do |f, h|
315
461
  v = instance_variable_get(f.ivar)
@@ -322,10 +468,15 @@ module Herringbone
322
468
  end
323
469
  end
324
470
 
471
+ # Structs are equal when they are of the same class and have the same field values
472
+ #
473
+ # @param other [Object] object to compare with
474
+ # @return [Boolean] true when +other+ is an equal struct
325
475
  def ==(other)
326
476
  other.class == self.class && other.to_h == to_h
327
477
  end
328
478
 
479
+ # @return [String] short class name and the set fields
329
480
  def inspect
330
481
  "#<#{self.class.name.split("::").last} #{to_h.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")}>"
331
482
  end