diff --git a/.gitignore b/.gitignore index 932fae0..2a815a9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ *.iml .idea +/vendor diff --git a/.ruby-version b/.ruby-version index 818bd47..fa7adc7 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.0.6 +3.3.5 diff --git a/lib/avromatic/model/raw_serialization.rb b/lib/avromatic/model/raw_serialization.rb index 395902f..dd4fbe8 100644 --- a/lib/avromatic/model/raw_serialization.rb +++ b/lib/avromatic/model/raw_serialization.rb @@ -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 diff --git a/lib/avromatic/model/types/abstract_type.rb b/lib/avromatic/model/types/abstract_type.rb index 7b85a5d..7ef9ef3 100644 --- a/lib/avromatic/model/types/abstract_type.rb +++ b/lib/avromatic/model/types/abstract_type.rb @@ -4,6 +4,7 @@ module Avromatic module Model module Types class AbstractType + EMPTY_ARRAY = [].freeze private_constant :EMPTY_ARRAY @@ -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 diff --git a/lib/avromatic/model/types/array_type.rb b/lib/avromatic/model/types/array_type.rb index 8394db9..c322a20 100644 --- a/lib/avromatic/model/types/array_type.rb +++ b/lib/avromatic/model/types/array_type.rb @@ -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) diff --git a/lib/avromatic/model/types/big_int_type.rb b/lib/avromatic/model/types/big_int_type.rb new file mode 100644 index 0000000..bea1046 --- /dev/null +++ b/lib/avromatic/model/types/big_int_type.rb @@ -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 diff --git a/lib/avromatic/model/types/boolean_type.rb b/lib/avromatic/model/types/boolean_type.rb index 3022f03..2988efd 100644 --- a/lib/avromatic/model/types/boolean_type.rb +++ b/lib/avromatic/model/types/boolean_type.rb @@ -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 diff --git a/lib/avromatic/model/types/date_type.rb b/lib/avromatic/model/types/date_type.rb index e296863..a9dfdbf 100644 --- a/lib/avromatic/model/types/date_type.rb +++ b/lib/avromatic/model/types/date_type.rb @@ -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 @@ -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 diff --git a/lib/avromatic/model/types/decimal_type.rb b/lib/avromatic/model/types/decimal_type.rb index 0218b0f..89cacbf 100644 --- a/lib/avromatic/model/types/decimal_type.rb +++ b/lib/avromatic/model/types/decimal_type.rb @@ -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 diff --git a/lib/avromatic/model/types/enum_type.rb b/lib/avromatic/model/types/enum_type.rb index 5521678..248c79b 100644 --- a/lib/avromatic/model/types/enum_type.rb +++ b/lib/avromatic/model/types/enum_type.rb @@ -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) diff --git a/lib/avromatic/model/types/float_type.rb b/lib/avromatic/model/types/float_type.rb index 6d6d257..76128e7 100644 --- a/lib/avromatic/model/types/float_type.rb +++ b/lib/avromatic/model/types/float_type.rb @@ -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 diff --git a/lib/avromatic/model/types/integer_type.rb b/lib/avromatic/model/types/integer_type.rb index 66365e1..92177e3 100644 --- a/lib/avromatic/model/types/integer_type.rb +++ b/lib/avromatic/model/types/integer_type.rb @@ -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 @@ -17,7 +23,7 @@ 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}") @@ -25,7 +31,7 @@ def coerce(input) end def coercible?(input) - input.nil? || input.is_a?(::Integer) + input.nil? || self.class.in_range?(input) end alias_method :coerced?, :coercible? diff --git a/lib/avromatic/model/types/record_type.rb b/lib/avromatic/model/types/record_type.rb index 82a9a79..44b5027 100644 --- a/lib/avromatic/model/types/record_type.rb +++ b/lib/avromatic/model/types/record_type.rb @@ -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 diff --git a/lib/avromatic/model/types/string_type.rb b/lib/avromatic/model/types/string_type.rb index 954c18d..0e5d585 100644 --- a/lib/avromatic/model/types/string_type.rb +++ b/lib/avromatic/model/types/string_type.rb @@ -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 diff --git a/lib/avromatic/model/types/type_factory.rb b/lib/avromatic/model/types/type_factory.rb index c6eae95..0fe4c12 100644 --- a/lib/avromatic/model/types/type_factory.rb +++ b/lib/avromatic/model/types/type_factory.rb @@ -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' @@ -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 diff --git a/lib/avromatic/model/types/union_type.rb b/lib/avromatic/model/types/union_type.rb index e2661a1..721b734 100644 --- a/lib/avromatic/model/types/union_type.rb +++ b/lib/avromatic/model/types/union_type.rb @@ -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 diff --git a/spec/avro/dsl/test/real_int_union.rb b/spec/avro/dsl/test/real_int_union.rb new file mode 100644 index 0000000..1f22815 --- /dev/null +++ b/spec/avro/dsl/test/real_int_union.rb @@ -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 diff --git a/spec/avro/schema/test/logical_types.avsc b/spec/avro/schema/test/logical_types.avsc index e17fca8..22d8d5d 100644 --- a/spec/avro/schema/test/logical_types.avsc +++ b/spec/avro/schema/test/logical_types.avsc @@ -24,6 +24,15 @@ "logicalType": "timestamp-micros" } }, + { + "name": "decimal", + "type": { + "type": "bytes", + "logicalType": "decimal", + "precision": 4, + "scale": 2 + } + }, { "name": "unknown", "type": { diff --git a/spec/avro/schema/test/real_int_union.avsc b/spec/avro/schema/test/real_int_union.avsc new file mode 100644 index 0000000..9c32a7e --- /dev/null +++ b/spec/avro/schema/test/real_int_union.avsc @@ -0,0 +1,19 @@ +{ + "type": "record", + "name": "real_int_union", + "namespace": "test", + "fields": [ + { + "name": "header", + "type": "string" + }, + { + "name": "message", + "type": [ + "int", + "string", + "long" + ] + } + ] +} diff --git a/spec/avromatic/io/datum_reader_spec.rb b/spec/avromatic/io/datum_reader_spec.rb index 1aa30b5..64df092 100644 --- a/spec/avromatic/io/datum_reader_spec.rb +++ b/spec/avromatic/io/datum_reader_spec.rb @@ -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, diff --git a/spec/avromatic/io/datum_writer_spec.rb b/spec/avromatic/io/datum_writer_spec.rb index 237bce7..d9f6f3a 100644 --- a/spec/avromatic/io/datum_writer_spec.rb +++ b/spec/avromatic/io/datum_writer_spec.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ed3c416..5c76535 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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