avro 1.10.2 → 1.11.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.
data/lib/avro/schema.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Licensed to the Apache Software Foundation (ASF) under one
2
3
  # or more contributor license agreements. See the NOTICE file
3
4
  # distributed with this work for additional information
@@ -29,7 +30,7 @@ module Avro
29
30
  NAMED_TYPES_SYM = Set.new(NAMED_TYPES.map(&:to_sym))
30
31
  VALID_TYPES_SYM = Set.new(VALID_TYPES.map(&:to_sym))
31
32
 
32
- NAME_REGEX = /^([A-Za-z_][A-Za-z0-9_]*)(\.([A-Za-z_][A-Za-z0-9_]*))*$/
33
+ NAME_REGEX = /^([A-Za-z_][A-Za-z0-9_]*)(\.([A-Za-z_][A-Za-z0-9_]*))*$/.freeze
33
34
 
34
35
  INT_MIN_VALUE = -(1 << 31)
35
36
  INT_MAX_VALUE = (1 << 31) - 1
@@ -38,6 +39,8 @@ module Avro
38
39
 
39
40
  DEFAULT_VALIDATE_OPTIONS = { recursive: true, encoded: false }.freeze
40
41
 
42
+ DECIMAL_LOGICAL_TYPE = 'decimal'
43
+
41
44
  def self.parse(json_string)
42
45
  real_parse(MultiJson.load(json_string), {})
43
46
  end
@@ -75,7 +78,9 @@ module Avro
75
78
  case type_sym
76
79
  when :fixed
77
80
  size = json_obj['size']
78
- return FixedSchema.new(name, namespace, size, names, logical_type, aliases)
81
+ precision = json_obj['precision']
82
+ scale = json_obj['scale']
83
+ return FixedSchema.new(name, namespace, size, names, logical_type, aliases, precision, scale)
79
84
  when :enum
80
85
  symbols = json_obj['symbols']
81
86
  doc = json_obj['doc']
@@ -131,7 +136,7 @@ module Avro
131
136
  def type; @type_sym.to_s; end
132
137
 
133
138
  def type_adapter
134
- @type_adapter ||= LogicalTypes.type_adapter(type, logical_type) || LogicalTypes::Identity
139
+ @type_adapter ||= LogicalTypes.type_adapter(type, logical_type, self) || LogicalTypes::Identity
135
140
  end
136
141
 
137
142
  # Returns the MD5 fingerprint of the schema as an Integer.
@@ -175,7 +180,7 @@ module Avro
175
180
  fp
176
181
  end
177
182
 
178
- SINGLE_OBJECT_MAGIC_NUMBER = [0xC3, 0x01]
183
+ SINGLE_OBJECT_MAGIC_NUMBER = [0xC3, 0x01].freeze
179
184
  def single_object_encoding_header
180
185
  [SINGLE_OBJECT_MAGIC_NUMBER, single_object_schema_fingerprint].flatten
181
186
  end
@@ -283,6 +288,10 @@ module Avro
283
288
  def match_fullname?(name)
284
289
  name == fullname || fullname_aliases.include?(name)
285
290
  end
291
+
292
+ def match_schema?(schema)
293
+ type_sym == schema.type_sym && match_fullname?(schema.fullname)
294
+ end
286
295
  end
287
296
 
288
297
  class RecordSchema < NamedSchema
@@ -413,7 +422,7 @@ module Avro
413
422
  end
414
423
 
415
424
  class EnumSchema < NamedSchema
416
- SYMBOL_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/
425
+ SYMBOL_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/.freeze
417
426
 
418
427
  attr_reader :symbols, :doc, :default
419
428
 
@@ -467,14 +476,27 @@ module Avro
467
476
  hsh = super
468
477
  hsh.size == 1 ? type : hsh
469
478
  end
479
+
480
+ def match_schema?(schema)
481
+ return type_sym == schema.type_sym
482
+ # TODO: eventually this could handle schema promotion for primitive schemas too
483
+ end
470
484
  end
471
485
 
472
486
  class BytesSchema < PrimitiveSchema
487
+ ERROR_INVALID_SCALE = 'Scale must be greater than or equal to 0'
488
+ ERROR_INVALID_PRECISION = 'Precision must be positive'
489
+ ERROR_PRECISION_TOO_SMALL = 'Precision must be greater than scale'
490
+
473
491
  attr_reader :precision, :scale
492
+
474
493
  def initialize(type, logical_type=nil, precision=nil, scale=nil)
475
494
  super(type.to_sym, logical_type)
476
- @precision = precision
477
- @scale = scale
495
+
496
+ @precision = precision.to_i if precision
497
+ @scale = scale.to_i if scale
498
+
499
+ validate_decimal! if logical_type == DECIMAL_LOGICAL_TYPE
478
500
  end
479
501
 
480
502
  def to_avro(names=nil)
@@ -485,29 +507,64 @@ module Avro
485
507
  avro['scale'] = scale if scale
486
508
  avro
487
509
  end
510
+
511
+ def match_schema?(schema)
512
+ return true if super
513
+
514
+ if logical_type == DECIMAL_LOGICAL_TYPE && schema.logical_type == DECIMAL_LOGICAL_TYPE
515
+ return precision == schema.precision && (scale || 0) == (schema.scale || 0)
516
+ end
517
+
518
+ false
519
+ end
520
+
521
+ private
522
+
523
+ def validate_decimal!
524
+ raise Avro::SchemaParseError, ERROR_INVALID_PRECISION unless precision.to_i.positive?
525
+ raise Avro::SchemaParseError, ERROR_INVALID_SCALE if scale.to_i.negative?
526
+ raise Avro::SchemaParseError, ERROR_PRECISION_TOO_SMALL if precision < scale.to_i
527
+ end
488
528
  end
489
529
 
490
530
  class FixedSchema < NamedSchema
491
- attr_reader :size
492
- def initialize(name, space, size, names=nil, logical_type=nil, aliases=nil)
531
+ attr_reader :size, :precision, :scale
532
+ def initialize(name, space, size, names=nil, logical_type=nil, aliases=nil, precision=nil, scale=nil)
493
533
  # Ensure valid cto args
494
534
  unless size.is_a?(Integer)
495
535
  raise AvroError, 'Fixed Schema requires a valid integer for size property.'
496
536
  end
497
537
  super(:fixed, name, space, names, nil, logical_type, aliases)
498
538
  @size = size
539
+ @precision = precision
540
+ @scale = scale
499
541
  end
500
542
 
501
543
  def to_avro(names=Set.new)
502
544
  avro = super
503
- avro.is_a?(Hash) ? avro.merge('size' => size) : avro
545
+ return avro if avro.is_a?(String)
546
+
547
+ avro['size'] = size
548
+ avro['precision'] = precision if precision
549
+ avro['scale'] = scale if scale
550
+ avro
551
+ end
552
+
553
+ def match_schema?(schema)
554
+ return true if super && size == schema.size
555
+
556
+ if logical_type == DECIMAL_LOGICAL_TYPE && schema.logical_type == DECIMAL_LOGICAL_TYPE
557
+ return precision == schema.precision && (scale || 0) == (schema.scale || 0)
558
+ end
559
+
560
+ false
504
561
  end
505
562
  end
506
563
 
507
564
  class Field < Schema
508
565
  attr_reader :type, :name, :default, :order, :doc, :aliases
509
566
 
510
- def initialize(type, name, default=:no_default, order=nil, names=nil, namespace=nil, doc=nil, aliases=nil)
567
+ def initialize(type, name, default=:no_default, order=nil, names=nil, namespace=nil, doc=nil, aliases=nil) # rubocop:disable Lint/MissingSuper
511
568
  @type = subparse(type, names, namespace)
512
569
  @name = name
513
570
  @default = default
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Licensed to the Apache Software Foundation (ASF) under one
2
3
  # or more contributor license agreements. See the NOTICE file
3
4
  # distributed with this work for additional information
@@ -46,28 +47,22 @@ module Avro
46
47
  end
47
48
 
48
49
  if w_type == r_type
49
- return true if Schema::PRIMITIVE_TYPES_SYM.include?(r_type)
50
+ return readers_schema.match_schema?(writers_schema) if Schema::PRIMITIVE_TYPES_SYM.include?(r_type)
50
51
 
51
52
  case r_type
52
- when :record
53
- return readers_schema.match_fullname?(writers_schema.fullname)
54
- when :error
55
- return readers_schema.match_fullname?(writers_schema.fullname)
56
53
  when :request
57
54
  return true
58
- when :fixed
59
- return readers_schema.match_fullname?(writers_schema.fullname) &&
60
- writers_schema.size == readers_schema.size
61
- when :enum
62
- return readers_schema.match_fullname?(writers_schema.fullname)
63
55
  when :map
64
56
  return match_schemas(writers_schema.values, readers_schema.values)
65
57
  when :array
66
58
  return match_schemas(writers_schema.items, readers_schema.items)
59
+ else
60
+ return readers_schema.match_schema?(writers_schema)
67
61
  end
68
62
  end
69
63
 
70
64
  # Handle schema promotion
65
+ # rubocop:disable Lint/DuplicateBranch
71
66
  if w_type == :int && INT_COERCIBLE_TYPES_SYM.include?(r_type)
72
67
  return true
73
68
  elsif w_type == :long && LONG_COERCIBLE_TYPES_SYM.include?(r_type)
@@ -79,8 +74,13 @@ module Avro
79
74
  elsif w_type == :bytes && r_type == :string
80
75
  return true
81
76
  end
77
+ # rubocop:enable Lint/DuplicateBranch
82
78
 
83
- return false
79
+ if readers_schema.respond_to?(:match_schema?)
80
+ readers_schema.match_schema?(writers_schema)
81
+ else
82
+ false
83
+ end
84
84
  end
85
85
 
86
86
  class Checker
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Licensed to the Apache Software Foundation (ASF) under one
2
3
  # or more contributor license agreements. See the NOTICE file
3
4
  # distributed with this work for additional information
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Licensed to the Apache Software Foundation (ASF) under one
2
3
  # or more contributor license agreements. See the NOTICE file
3
4
  # distributed with this work for additional information
@@ -16,19 +17,19 @@
16
17
 
17
18
  module Avro
18
19
  class SchemaValidator
19
- ROOT_IDENTIFIER = '.'.freeze
20
- PATH_SEPARATOR = '.'.freeze
21
- INT_RANGE = Schema::INT_MIN_VALUE..Schema::INT_MAX_VALUE
22
- LONG_RANGE = Schema::LONG_MIN_VALUE..Schema::LONG_MAX_VALUE
20
+ ROOT_IDENTIFIER = '.'
21
+ PATH_SEPARATOR = '.'
22
+ INT_RANGE = (Schema::INT_MIN_VALUE..Schema::INT_MAX_VALUE).freeze
23
+ LONG_RANGE = (Schema::LONG_MIN_VALUE..Schema::LONG_MAX_VALUE).freeze
23
24
  COMPLEX_TYPES = [:array, :error, :map, :record, :request].freeze
24
25
  BOOLEAN_VALUES = [true, false].freeze
25
26
  DEFAULT_VALIDATION_OPTIONS = { recursive: true, encoded: false, fail_on_extra_fields: false }.freeze
26
27
  RECURSIVE_SIMPLE_VALIDATION_OPTIONS = { encoded: true }.freeze
27
28
  RUBY_CLASS_TO_AVRO_TYPE = {
28
- NilClass => 'null'.freeze,
29
- String => 'string'.freeze,
30
- Float => 'float'.freeze,
31
- Hash => 'record'.freeze
29
+ NilClass => 'null',
30
+ String => 'string',
31
+ Float => 'float',
32
+ Hash => 'record'
32
33
  }.freeze
33
34
 
34
35
  class Result
@@ -132,7 +133,7 @@ module Avro
132
133
  fail TypeMismatchError unless datum.is_a?(Integer)
133
134
  result.add_error(path, "out of bound value #{datum}") unless LONG_RANGE.cover?(datum)
134
135
  when :float, :double
135
- fail TypeMismatchError unless datum.is_a?(Float) || datum.is_a?(Integer)
136
+ fail TypeMismatchError unless datum.is_a?(Float) || datum.is_a?(Integer) || datum.is_a?(BigDecimal)
136
137
  when :fixed
137
138
  if datum.is_a? String
138
139
  result.add_error(path, fixed_string_message(expected_schema.size, datum)) unless datum.bytesize == expected_schema.size
@@ -217,9 +218,9 @@ module Avro
217
218
  end
218
219
 
219
220
  def deeper_path_for_hash(sub_key, path)
220
- deeper_path = "#{path}#{PATH_SEPARATOR}#{sub_key}"
221
+ deeper_path = +"#{path}#{PATH_SEPARATOR}#{sub_key}"
221
222
  deeper_path.squeeze!(PATH_SEPARATOR)
222
- deeper_path
223
+ deeper_path.freeze
223
224
  end
224
225
 
225
226
  def actual_value_message(value)
@@ -240,7 +241,7 @@ module Avro
240
241
  end
241
242
 
242
243
  def ruby_integer_to_avro_type(value)
243
- INT_RANGE.cover?(value) ? 'int'.freeze : 'long'.freeze
244
+ INT_RANGE.cover?(value) ? 'int' : 'long'
244
245
  end
245
246
  end
246
247
  end
data/lib/avro.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Licensed to the Apache Software Foundation (ASF) under one
2
3
  # or more contributor license agreements. See the NOTICE file
3
4
  # distributed with this work for additional information
data/test/case_finder.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  #
2
3
  # Licensed to the Apache Software Foundation (ASF) under one
3
4
  # or more contributor license agreements. See the NOTICE file
data/test/random_data.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Licensed to the Apache Software Foundation (ASF) under one
2
3
  # or more contributor license agreements. See the NOTICE file
3
4
  # distributed with this work for additional information
@@ -5,9 +6,9 @@
5
6
  # to you under the Apache License, Version 2.0 (the
6
7
  # "License"); you may not use this file except in compliance
7
8
  # with the License. You may obtain a copy of the License at
8
- #
9
+ #
9
10
  # https://www.apache.org/licenses/LICENSE-2.0
10
- #
11
+ #
11
12
  # Unless required by applicable law or agreed to in writing, software
12
13
  # distributed under the License is distributed on an "AS IS" BASIS,
13
14
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -74,7 +75,7 @@ class RandomData
74
75
  return nil if len == 0
75
76
  symbols[rand(len)]
76
77
  when :fixed
77
- f = ""
78
+ f = +""
78
79
  schm.size.times { f << BYTEPOOL[rand(BYTEPOOL.size), 1] }
79
80
  f
80
81
  end
@@ -95,7 +96,7 @@ class RandomData
95
96
  BYTEPOOL = '12345abcd'
96
97
 
97
98
  def randstr(chars=CHARPOOL, length=20)
98
- str = ''
99
+ str = +''
99
100
  rand(length+1).times { str << chars[rand(chars.size)] }
100
101
  str
101
102
  end
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
  # Licensed to the Apache Software Foundation (ASF) under one
3
4
  # or more contributor license agreements. See the NOTICE file
4
5
  # distributed with this work for additional information
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
  # Licensed to the Apache Software Foundation (ASF) under one
3
4
  # or more contributor license agreements. See the NOTICE file
4
5
  # distributed with this work for additional information
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Licensed to the Apache Software Foundation (ASF) under one
2
3
  # or more contributor license agreements. See the NOTICE file
3
4
  # distributed with this work for additional information
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
  # Licensed to the Apache Software Foundation (ASF) under one
3
4
  # or more contributor license agreements. See the NOTICE file
4
5
  # distributed with this work for additional information
@@ -1,4 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
2
3
  # Licensed to the Apache Software Foundation (ASF) under one
3
4
  # or more contributor license agreements. See the NOTICE file
4
5
  # distributed with this work for additional information
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Licensed to the Apache Software Foundation (ASF) under one
2
3
  # or more contributor license agreements. See the NOTICE file
3
4
  # distributed with this work for additional information
data/test/test_help.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Licensed to the Apache Software Foundation (ASF) under one
2
3
  # or more contributor license agreements. See the NOTICE file
3
4
  # distributed with this work for additional information
data/test/test_io.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Licensed to the Apache Software Foundation (ASF) under one
2
3
  # or more contributor license agreements. See the NOTICE file
3
4
  # distributed with this work for additional information
@@ -217,7 +218,7 @@ EOS
217
218
  [64, '80 01'],
218
219
  [8192, '80 80 01'],
219
220
  [-8193, '81 80 01'],
220
- ]
221
+ ].freeze
221
222
 
222
223
  def avro_hexlify(reader)
223
224
  bytes = []
@@ -266,46 +267,46 @@ EOS
266
267
 
267
268
  def test_utf8_string_encoding
268
269
  [
269
- "\xC3".force_encoding('ISO-8859-1'),
270
- "\xC3\x83".force_encoding('UTF-8')
270
+ String.new("\xC3", encoding: 'ISO-8859-1'),
271
+ String.new("\xC3\x83", encoding: 'UTF-8')
271
272
  ].each do |value|
272
- output = ''.force_encoding('BINARY')
273
+ output = String.new('', encoding: 'BINARY')
273
274
  encoder = Avro::IO::BinaryEncoder.new(StringIO.new(output))
274
275
  datum_writer = Avro::IO::DatumWriter.new(Avro::Schema.parse('"string"'))
275
276
  datum_writer.write(value, encoder)
276
277
 
277
- assert_equal "\x04\xc3\x83".force_encoding('BINARY'), output
278
+ assert_equal String.new("\x04\xc3\x83", encoding: 'BINARY'), output
278
279
  end
279
280
  end
280
281
 
281
282
  def test_bytes_encoding
282
283
  [
283
- "\xC3\x83".force_encoding('BINARY'),
284
- "\xC3\x83".force_encoding('ISO-8859-1'),
285
- "\xC3\x83".force_encoding('UTF-8')
284
+ String.new("\xC3\x83", encoding: 'BINARY'),
285
+ String.new("\xC3\x83", encoding: 'ISO-8859-1'),
286
+ String.new("\xC3\x83", encoding: 'UTF-8')
286
287
  ].each do |value|
287
- output = ''.force_encoding('BINARY')
288
+ output = String.new('', encoding: 'BINARY')
288
289
  encoder = Avro::IO::BinaryEncoder.new(StringIO.new(output))
289
290
  datum_writer = Avro::IO::DatumWriter.new(Avro::Schema.parse('"bytes"'))
290
291
  datum_writer.write(value, encoder)
291
292
 
292
- assert_equal "\x04\xc3\x83".force_encoding('BINARY'), output
293
+ assert_equal String.new("\x04\xc3\x83", encoding: 'BINARY'), output
293
294
  end
294
295
  end
295
296
 
296
297
  def test_fixed_encoding
297
298
  [
298
- "\xC3\x83".force_encoding('BINARY'),
299
- "\xC3\x83".force_encoding('ISO-8859-1'),
300
- "\xC3\x83".force_encoding('UTF-8')
299
+ String.new("\xC3\x83", encoding: 'BINARY'),
300
+ String.new("\xC3\x83", encoding: 'ISO-8859-1'),
301
+ String.new("\xC3\x83", encoding: 'UTF-8')
301
302
  ].each do |value|
302
- output = ''.force_encoding('BINARY')
303
+ output = String.new('', encoding: 'BINARY')
303
304
  encoder = Avro::IO::BinaryEncoder.new(StringIO.new(output))
304
305
  schema = '{"type": "fixed", "name": "TwoBytes", "size": 2}'
305
306
  datum_writer = Avro::IO::DatumWriter.new(Avro::Schema.parse(schema))
306
307
  datum_writer.write(value, encoder)
307
308
 
308
- assert_equal "\xc3\x83".force_encoding('BINARY'), output
309
+ assert_equal String.new("\xC3\x83", encoding: 'BINARY'), output
309
310
  end
310
311
  end
311
312
 
@@ -489,6 +490,20 @@ EOS
489
490
  assert_equal(datum_read, { 'field2' => 1 })
490
491
  end
491
492
 
493
+ def test_big_decimal_datum_for_float
494
+ writers_schema = Avro::Schema.parse('"float"')
495
+ writer, * = write_datum(BigDecimal('1.2'), writers_schema)
496
+ datum_read = read_datum(writer, writers_schema)
497
+ assert_in_delta(1.2, datum_read)
498
+ end
499
+
500
+ def test_big_decimal_datum_for_double
501
+ writers_schema = Avro::Schema.parse('"double"')
502
+ writer, * = write_datum(BigDecimal("1.2"), writers_schema)
503
+ datum_read = read_datum(writer, writers_schema)
504
+ assert_in_delta(1.2, datum_read)
505
+ end
506
+
492
507
  def test_snappy_backward_compat
493
508
  # a snappy-compressed block payload without the checksum
494
509
  # this has no back-references, just one literal so the last 9
@@ -567,7 +582,7 @@ EOS
567
582
  datum = randomdata.next
568
583
  assert validate(schm, datum), 'datum is not valid for schema'
569
584
  w = Avro::IO::DatumWriter.new(schm)
570
- writer = StringIO.new "", "w"
585
+ writer = StringIO.new(+"", "w")
571
586
  w.write(datum, Avro::IO::BinaryEncoder.new(writer))
572
587
  r = datum_reader(schm)
573
588
  reader = StringIO.new(writer.string)
@@ -1,4 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
2
3
  # Licensed to the Apache Software Foundation (ASF) under one
3
4
  # or more contributor license agreements. See the NOTICE file
4
5
  # distributed with this work for additional information
@@ -16,6 +17,7 @@
16
17
  # limitations under the License.
17
18
 
18
19
  require 'test_help'
20
+ require 'memory_profiler'
19
21
 
20
22
  class TestLogicalTypes < Test::Unit::TestCase
21
23
  def test_int_date
@@ -98,8 +100,143 @@ class TestLogicalTypes < Test::Unit::TestCase
98
100
  assert_equal 'duration', schema.logical_type
99
101
  end
100
102
 
103
+ def test_bytes_decimal
104
+ schema = Avro::Schema.parse <<-SCHEMA
105
+ { "type": "bytes", "logicalType": "decimal", "precision": 9, "scale": 6 }
106
+ SCHEMA
107
+
108
+ assert_equal 'decimal', schema.logical_type
109
+ assert_equal 9, schema.precision
110
+ assert_equal 6, schema.scale
111
+
112
+ assert_encode_and_decode BigDecimal('-3.4562'), schema
113
+ assert_encode_and_decode BigDecimal('3.4562'), schema
114
+ assert_encode_and_decode 15.123, schema
115
+ assert_encode_and_decode 15, schema
116
+ assert_encode_and_decode BigDecimal('0.123456'), schema
117
+ assert_encode_and_decode BigDecimal('0'), schema
118
+ assert_encode_and_decode BigDecimal('1'), schema
119
+ assert_encode_and_decode BigDecimal('-1'), schema
120
+
121
+ assert_raise ArgumentError do
122
+ type = Avro::LogicalTypes::BytesDecimal.new(schema)
123
+ type.encode('1.23')
124
+ end
125
+ end
126
+
127
+ def test_bytes_decimal_range_errors
128
+ schema = Avro::Schema.parse <<-SCHEMA
129
+ { "type": "bytes", "logicalType": "decimal", "precision": 4, "scale": 2 }
130
+ SCHEMA
131
+
132
+ type = Avro::LogicalTypes::BytesDecimal.new(schema)
133
+
134
+ assert_raises RangeError do
135
+ type.encode(BigDecimal('345'))
136
+ end
137
+
138
+ assert_raises RangeError do
139
+ type.encode(BigDecimal('1.5342'))
140
+ end
141
+
142
+ assert_raises RangeError do
143
+ type.encode(BigDecimal('-1.5342'))
144
+ end
145
+
146
+ assert_raises RangeError do
147
+ type.encode(BigDecimal('-100.2'))
148
+ end
149
+
150
+ assert_raises RangeError do
151
+ type.encode(BigDecimal('-99.991'))
152
+ end
153
+ end
154
+
155
+ def test_bytes_decimal_conversion
156
+ schema = Avro::Schema.parse <<-SCHEMA
157
+ { "type": "bytes", "logicalType": "decimal", "precision": 12, "scale": 6 }
158
+ SCHEMA
159
+
160
+ type = Avro::LogicalTypes::BytesDecimal.new(schema)
161
+
162
+ enc = "\xcb\x43\x38".dup.force_encoding('BINARY')
163
+ assert_equal enc, type.encode(BigDecimal('-3.4562'))
164
+ assert_equal BigDecimal('-3.4562'), type.decode(enc)
165
+
166
+ assert_equal "\x34\xbc\xc8".dup.force_encoding('BINARY'), type.encode(BigDecimal('3.4562'))
167
+ assert_equal BigDecimal('3.4562'), type.decode("\x34\xbc\xc8".dup.force_encoding('BINARY'))
168
+
169
+ assert_equal "\x6a\x33\x0e\x87\x00".dup.force_encoding('BINARY'), type.encode(BigDecimal('456123.123456'))
170
+ assert_equal BigDecimal('456123.123456'), type.decode("\x6a\x33\x0e\x87\x00".dup.force_encoding('BINARY'))
171
+ end
172
+
173
+ def test_logical_type_with_schema
174
+ exception = assert_raises(ArgumentError) do
175
+ Avro::LogicalTypes::LogicalTypeWithSchema.new(nil)
176
+ end
177
+ assert_equal exception.to_s, 'schema is required'
178
+
179
+ schema = Avro::Schema.parse <<-SCHEMA
180
+ { "type": "bytes", "logicalType": "decimal", "precision": 12, "scale": 6 }
181
+ SCHEMA
182
+
183
+ assert_nothing_raised do
184
+ Avro::LogicalTypes::LogicalTypeWithSchema.new(schema)
185
+ end
186
+
187
+ assert_raises NotImplementedError do
188
+ Avro::LogicalTypes::LogicalTypeWithSchema.new(schema).encode(BigDecimal('2'))
189
+ end
190
+
191
+ assert_raises NotImplementedError do
192
+ Avro::LogicalTypes::LogicalTypeWithSchema.new(schema).decode('foo')
193
+ end
194
+ end
195
+
196
+ def test_bytes_decimal_object_allocations_encode
197
+ schema = Avro::Schema.parse <<-SCHEMA
198
+ { "type": "bytes", "logicalType": "decimal", "precision": 4, "scale": 2 }
199
+ SCHEMA
200
+
201
+ type = Avro::LogicalTypes::BytesDecimal.new(schema)
202
+
203
+ positive_value = BigDecimal('5.2')
204
+ negative_value = BigDecimal('-5.2')
205
+
206
+ [positive_value, negative_value].each do |value|
207
+ report = MemoryProfiler.report do
208
+ type.encode(value)
209
+ end
210
+
211
+ assert_equal 5, report.total_allocated
212
+ # Ruby 2.7 does not retain anything. Ruby 2.6 retains 1
213
+ assert_operator 1, :>=, report.total_retained
214
+ end
215
+ end
216
+
217
+ def test_bytes_decimal_object_allocations_decode
218
+ schema = Avro::Schema.parse <<-SCHEMA
219
+ { "type": "bytes", "logicalType": "decimal", "precision": 4, "scale": 2 }
220
+ SCHEMA
221
+
222
+ type = Avro::LogicalTypes::BytesDecimal.new(schema)
223
+
224
+ positive_enc = "\x02\b".dup.force_encoding('BINARY')
225
+ negative_enc = "\xFD\xF8".dup.force_encoding('BINARY')
226
+
227
+ [positive_enc, negative_enc].each do |encoded|
228
+ report = MemoryProfiler.report do
229
+ type.decode(encoded)
230
+ end
231
+
232
+ assert_equal 5, report.total_allocated
233
+ # Ruby 2.7 does not retain anything. Ruby 2.6 retains 1
234
+ assert_operator 1, :>=, report.total_retained
235
+ end
236
+ end
237
+
101
238
  def encode(datum, schema)
102
- buffer = StringIO.new("")
239
+ buffer = StringIO.new
103
240
  encoder = Avro::IO::BinaryEncoder.new(buffer)
104
241
 
105
242
  datum_writer = Avro::IO::DatumWriter.new(schema)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Licensed to the Apache Software Foundation (ASF) under one
2
3
  # or more contributor license agreements. See the NOTICE file
3
4
  # distributed with this work for additional information
@@ -184,7 +185,7 @@ EOS
184
185
  }
185
186
  }
186
187
  EOS
187
- ]
188
+ ].freeze
188
189
 
189
190
  Protocol = Avro::Protocol
190
191
  def test_parse