Skip to content

Commit

Permalink
Improve support for BigInt, enhance union type (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
opti authored Nov 19, 2024
1 parent 5899c5e commit 9653f8a
Show file tree
Hide file tree
Showing 22 changed files with 174 additions and 54 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@

*.iml
.idea
/vendor
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.0.6
3.3.5
2 changes: 2 additions & 0 deletions lib/avromatic/model/raw_serialization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ module Encode

UNSPECIFIED = Object.new

# rubocop:disable Style/AccessModifierDeclarations
delegate :datum_writer, :datum_reader, to: :class
private :datum_writer, :datum_reader
# rubocop:enable Style/AccessModifierDeclarations

def avro_raw_value(validate: UNSPECIFIED)
unless validate == UNSPECIFIED
Expand Down
9 changes: 5 additions & 4 deletions lib/avromatic/model/types/abstract_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module Avromatic
module Model
module Types
class AbstractType

EMPTY_ARRAY = [].freeze
private_constant :EMPTY_ARRAY

Expand All @@ -23,12 +24,12 @@ def coerce(_input)
raise "#{__method__} must be overridden by #{self.class.name}"
end

def coercible?(_input)
raise "#{__method__} must be overridden by #{self.class.name}"
def coercible?(input)
input.nil? || input_classes.any? { |input_class| input.is_a?(input_class) }
end

def coerced?(_value)
raise "#{__method__} must be overridden by #{self.class.name}"
def coerced?(value)
value.nil? || value_classes.any? { |value_class| value.is_a?(value_class) }
end

# Note we use positional args rather than keyword args to reduce
Expand Down
2 changes: 1 addition & 1 deletion lib/avromatic/model/types/array_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def coercible?(input)
end

def coerced?(value)
value.nil? || (value.is_a?(::Array) && value.all? { |element| value_type.coerced?(element) })
value.nil? || (value.is_a?(::Array) && value.all? { |element_input| value_type.coerced?(element_input) })
end

def serialize(value, strict)
Expand Down
49 changes: 49 additions & 0 deletions lib/avromatic/model/types/big_int_type.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

require 'avromatic/model/types/abstract_type'

module Avromatic
module Model
module Types
class BigIntType < AbstractType
VALUE_CLASSES = [::Integer].freeze

MAX_RANGE = 2**63

def self.in_range?(value)
value.is_a?(::Integer) && value.between?(-MAX_RANGE, MAX_RANGE - 1)
end

def value_classes
VALUE_CLASSES
end

def name
'bigint'
end

def coerce(input)
if coercible?(input)
input
else
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
end
end

def coercible?(input)
input.nil? || self.class.in_range?(input)
end

alias_method :coerced?, :coercible?

def serialize(value, _strict)
value
end

def referenced_model_classes
EMPTY_ARRAY
end
end
end
end
end
6 changes: 0 additions & 6 deletions lib/avromatic/model/types/boolean_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ def coerce(input)
end
end

def coercible?(input)
input.nil? || input.is_a?(::TrueClass) || input.is_a?(::FalseClass)
end

alias_method :coerced?, :coercible?

def serialize(value, _strict)
value
end
Expand Down
11 changes: 5 additions & 6 deletions lib/avromatic/model/types/date_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ module Model
module Types
class DateType < AbstractType
VALUE_CLASSES = [::Date].freeze
INPUT_CLASSES = [::Date, ::Time].freeze

def value_classes
VALUE_CLASSES
end

def input_classes
INPUT_CLASSES
end

def name
'date'
end
Expand All @@ -26,12 +31,6 @@ def coerce(input)
end
end

def coercible?(input)
input.nil? || input.is_a?(::Date) || input.is_a?(::Time)
end

alias_method :coerced?, :coercible?

def serialize(value, _strict)
value
end
Expand Down
8 changes: 0 additions & 8 deletions lib/avromatic/model/types/decimal_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,6 @@ def coerce(input)
end
end

def coercible?(input)
input.nil? || input_classes.any? { |input_class| input.is_a?(input_class) }
end

def coerced?(value)
value.nil? || value_classes.any? { |value_class| value.is_a?(value_class) }
end

def serialize(value, _strict)
value
end
Expand Down
4 changes: 2 additions & 2 deletions lib/avromatic/model/types/enum_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ def coerce(input)
end
end

def coerced?(input)
input.nil? || (input.is_a?(::String) && allowed_values.include?(input))
def coerced?(value)
value.nil? || value.is_a?(::String) && allowed_values.include?(value)
end

def coercible?(input)
Expand Down
8 changes: 0 additions & 8 deletions lib/avromatic/model/types/float_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,6 @@ def coerce(input)
end
end

def coercible?(input)
input.nil? || input.is_a?(::Float) || input.is_a?(::Integer)
end

def coerced?(input)
input.nil? || input.is_a?(::Float)
end

def serialize(value, _strict)
value
end
Expand Down
10 changes: 8 additions & 2 deletions lib/avromatic/model/types/integer_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ module Types
class IntegerType < AbstractType
VALUE_CLASSES = [::Integer].freeze

MAX_RANGE = 2**31

def self.in_range?(value)
value.is_a?(::Integer) && value.between?(-MAX_RANGE, MAX_RANGE - 1)
end

def value_classes
VALUE_CLASSES
end
Expand All @@ -17,15 +23,15 @@ def name
end

def coerce(input)
if input.nil? || input.is_a?(::Integer)
if coercible?(input)
input
else
raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
end
end

def coercible?(input)
input.nil? || input.is_a?(::Integer)
input.nil? || self.class.in_range?(input)
end

alias_method :coerced?, :coercible?
Expand Down
4 changes: 0 additions & 4 deletions lib/avromatic/model/types/record_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ def coercible?(input)
false
end

def coerced?(value)
value.nil? || value.is_a?(record_class)
end

def serialize(value, strict)
if value.nil?
value
Expand Down
8 changes: 0 additions & 8 deletions lib/avromatic/model/types/string_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,6 @@ def coerce(input)
end
end

def coercible?(input)
input.nil? || input.is_a?(::String) || input.is_a?(::Symbol)
end

def coerced?(value)
value.nil? || value.is_a?(::String)
end

def serialize(value, _strict)
value
end
Expand Down
3 changes: 2 additions & 1 deletion lib/avromatic/model/types/type_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
require 'avromatic/model/types/fixed_type'
require 'avromatic/model/types/float_type'
require 'avromatic/model/types/integer_type'
require 'avromatic/model/types/big_int_type'
require 'avromatic/model/types/map_type'
require 'avromatic/model/types/null_type'
require 'avromatic/model/types/record_type'
Expand All @@ -31,7 +32,7 @@ module TypeFactory
'bytes' => Avromatic::Model::Types::StringType.new,
'boolean' => Avromatic::Model::Types::BooleanType.new,
'int' => Avromatic::Model::Types::IntegerType.new,
'long' => Avromatic::Model::Types::IntegerType.new,
'long' => Avromatic::Model::Types::BigIntType.new,
'float' => Avromatic::Model::Types::FloatType.new,
'double' => Avromatic::Model::Types::FloatType.new,
'null' => Avromatic::Model::Types::NullType.new
Expand Down
10 changes: 9 additions & 1 deletion lib/avromatic/model/types/union_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,15 @@ def referenced_model_classes
private

def find_index(value)
# TODO: Cache this?
if value.is_a?(::Integer)
if Avromatic::Model::Types::IntegerType.in_range?(value)
return member_types.index { |member_type| member_type.is_a?(Avromatic::Model::Types::IntegerType) }
elsif Avromatic::Model::Types::BigIntType.in_range?(value)
return member_types.index { |member_type| member_type.is_a?(Avromatic::Model::Types::BigIntType) }
end
end

# TODO: Consider caching the index for each value class
member_types.find_index do |member_type|
member_type.value_classes.any? { |value_class| value.is_a?(value_class) }
end
Expand Down
8 changes: 8 additions & 0 deletions spec/avro/dsl/test/real_int_union.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

namespace :test

record :real_int_union do
required :header, :string
required :message, :union, types: [:int, :string, :long]
end
9 changes: 9 additions & 0 deletions spec/avro/schema/test/logical_types.avsc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@
"logicalType": "timestamp-micros"
}
},
{
"name": "decimal",
"type": {
"type": "bytes",
"logicalType": "decimal",
"precision": 4,
"scale": 2
}
},
{
"name": "unknown",
"type": {
Expand Down
19 changes: 19 additions & 0 deletions spec/avro/schema/test/real_int_union.avsc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"type": "record",
"name": "real_int_union",
"namespace": "test",
"fields": [
{
"name": "header",
"type": "string"
},
{
"name": "message",
"type": [
"int",
"string",
"long"
]
}
]
}
2 changes: 1 addition & 1 deletion spec/avromatic/io/datum_reader_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
b: '123',
tf: true,
i: rand(10),
l: 123456789,
l: 2**40,
f: 0.5,
d: 1.0 / 3.0,
n: nil,
Expand Down
51 changes: 51 additions & 0 deletions spec/avromatic/io/datum_writer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,55 @@
end
end
end

context "with int/bigint union" do
let(:schema_name) { 'test.real_int_union' }

context "for int" do
let(:values) do
{
header: 'foo',
message: 1
}
end

describe "#write_union" do
before { datum_writer.write_union(union_schema, datum, encoder) }

context "when the datum includes union member index" do
it "does not call Avro::Schema.validate" do
expect(Avro::Schema).not_to have_received(:validate)
end

it "calls write_data to encode the union" do
expect(datum_writer).to have_received(:write_data).with(union_schema.schemas[0], datum.datum, encoder)
end
end
end
end

context "for big int" do
let(:values) do
{
header: 'foo',
message: 1587961247350867973
}
end

describe "#write_union" do
before { datum_writer.write_union(union_schema, datum, encoder) }

context "when the datum includes union member index" do
it "does not call Avro::Schema.validate" do
expect(Avro::Schema).not_to have_received(:validate)
end

it "calls write_data to encode the union" do
expect(datum_writer).to have_received(:write_data).with(union_schema.schemas[2], datum.datum, encoder)
end
end
end
end
end

end
2 changes: 1 addition & 1 deletion spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

config.before do
Avromatic.logger = Logger.new('log/test.log')
Avromatic.registry_url = 'http://username:password@registry.example.com'
Avromatic.registry_url = 'http://username:password@registry.localhost'
Avromatic.use_schema_fingerprint_lookup = true
Avromatic.schema_store = AvroTurf::SchemaStore.new(path: 'spec/avro/schema')
Avromatic.custom_type_registry.clear
Expand Down

0 comments on commit 9653f8a

Please sign in to comment.