avromatic 2.2.2 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +85 -0
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/Appraisals +34 -22
- data/CHANGELOG.md +16 -0
- data/README.md +3 -3
- data/avromatic.gemspec +7 -6
- data/bin/console +4 -3
- data/gemfiles/{rails5_0.gemfile → avro1_10_rails5_2.gemfile} +3 -3
- data/gemfiles/avro1_10_rails6_0.gemfile +9 -0
- data/gemfiles/avro1_10_rails6_1.gemfile +9 -0
- data/gemfiles/{rails5_2.gemfile → avro1_8_rails5_2.gemfile} +0 -0
- data/gemfiles/{rails5_1.gemfile → avro1_9_rails5_2.gemfile} +3 -3
- data/gemfiles/{rails6_0.gemfile → avro1_9_rails6_0.gemfile} +1 -1
- data/gemfiles/{avro_patches_rails5_0.gemfile → avro1_9_rails6_1.gemfile} +3 -3
- data/gemfiles/avro_patches_rails5_2.gemfile +1 -1
- data/gemfiles/avro_patches_rails6_1.gemfile +9 -0
- data/lib/avromatic.rb +0 -5
- data/lib/avromatic/io.rb +1 -7
- data/lib/avromatic/io/datum_reader.rb +18 -68
- data/lib/avromatic/io/datum_writer.rb +5 -11
- data/lib/avromatic/io/union_datum.rb +25 -0
- data/lib/avromatic/messaging.rb +4 -2
- data/lib/avromatic/model/attributes.rb +6 -6
- data/lib/avromatic/model/configurable.rb +24 -0
- data/lib/avromatic/model/messaging_serialization.rb +2 -1
- data/lib/avromatic/model/raw_serialization.rb +14 -11
- data/lib/avromatic/model/types/abstract_timestamp_type.rb +1 -1
- data/lib/avromatic/model/types/abstract_type.rb +3 -1
- data/lib/avromatic/model/types/array_type.rb +2 -2
- data/lib/avromatic/model/types/boolean_type.rb +1 -1
- data/lib/avromatic/model/types/custom_type.rb +1 -1
- data/lib/avromatic/model/types/date_type.rb +1 -1
- data/lib/avromatic/model/types/enum_type.rb +1 -1
- data/lib/avromatic/model/types/fixed_type.rb +1 -1
- data/lib/avromatic/model/types/float_type.rb +1 -1
- data/lib/avromatic/model/types/integer_type.rb +1 -1
- data/lib/avromatic/model/types/map_type.rb +2 -2
- data/lib/avromatic/model/types/null_type.rb +1 -1
- data/lib/avromatic/model/types/record_type.rb +1 -5
- data/lib/avromatic/model/types/string_type.rb +1 -1
- data/lib/avromatic/model/types/timestamp_micros_type.rb +4 -4
- data/lib/avromatic/model/types/timestamp_millis_type.rb +4 -4
- data/lib/avromatic/model/types/union_type.rb +11 -8
- data/lib/avromatic/version.rb +1 -1
- metadata +62 -47
- data/.travis.yml +0 -23
- data/gemfiles/avro_patches_rails5_1.gemfile +0 -9
- data/lib/avromatic/patches.rb +0 -18
- data/lib/avromatic/patches/schema_validator_patch.rb +0 -39
@@ -7,10 +7,11 @@ module Avromatic
|
|
7
7
|
class DatumWriter < Avro::IO::DatumWriter
|
8
8
|
def write_union(writers_schema, datum, encoder)
|
9
9
|
optional = writers_schema.schemas.first.type_sym == :null
|
10
|
-
if datum.is_a?(
|
11
|
-
index_of_schema = datum
|
10
|
+
if datum.is_a?(Avromatic::IO::UnionDatum)
|
11
|
+
index_of_schema = datum.member_index
|
12
12
|
# Avromatic does not treat the null of an optional field as part of the union
|
13
13
|
index_of_schema += 1 if optional
|
14
|
+
datum = datum.datum
|
14
15
|
elsif optional && writers_schema.schemas.size == 2
|
15
16
|
# Optimize for the common case of a union that's just an optional field
|
16
17
|
index_of_schema = datum.nil? ? 0 : 1
|
@@ -19,21 +20,14 @@ module Avromatic
|
|
19
20
|
Avro::Schema.validate(schema, datum)
|
20
21
|
end
|
21
22
|
end
|
23
|
+
|
22
24
|
unless index_of_schema
|
23
25
|
raise Avro::IO::AvroTypeError.new(writers_schema, datum)
|
24
26
|
end
|
27
|
+
|
25
28
|
encoder.write_long(index_of_schema)
|
26
29
|
write_data(writers_schema.schemas[index_of_schema], datum, encoder)
|
27
30
|
end
|
28
|
-
|
29
|
-
def write_record(writers_schema, datum, encoder)
|
30
|
-
if datum.is_a?(Hash) && datum.key?(Avromatic::IO::ENCODING_PROVIDER)
|
31
|
-
# This is only used for recursive serialization so validation has already been done
|
32
|
-
encoder.write(datum[Avromatic::IO::ENCODING_PROVIDER].avro_raw_value(validate: false))
|
33
|
-
else
|
34
|
-
super
|
35
|
-
end
|
36
|
-
end
|
37
31
|
end
|
38
32
|
end
|
39
33
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Avromatic
|
4
|
+
module IO
|
5
|
+
class UnionDatum
|
6
|
+
attr_reader :member_index, :datum
|
7
|
+
|
8
|
+
def initialize(member_index, datum)
|
9
|
+
@member_index = member_index
|
10
|
+
@datum = datum
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
other.is_a?(Avromatic::IO::UnionDatum) &&
|
15
|
+
member_index == other.member_index &&
|
16
|
+
datum == other.datum
|
17
|
+
end
|
18
|
+
alias_method :eql?, :==
|
19
|
+
|
20
|
+
def hash
|
21
|
+
31 * datum.hash + member_index
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/avromatic/messaging.rb
CHANGED
@@ -29,7 +29,8 @@ module Avromatic
|
|
29
29
|
end
|
30
30
|
|
31
31
|
# The following line differs from the parent class to use a custom DatumReader
|
32
|
-
|
32
|
+
reader_class = Avromatic.use_custom_datum_reader ? Avromatic::IO::DatumReader : Avro::IO::DatumReader
|
33
|
+
reader = reader_class.new(writers_schema, readers_schema)
|
33
34
|
reader.read(decoder)
|
34
35
|
end
|
35
36
|
|
@@ -50,7 +51,8 @@ module Avromatic
|
|
50
51
|
encoder.write([schema_id].pack('N'))
|
51
52
|
|
52
53
|
# The following line differs from the parent class to use a custom DatumWriter
|
53
|
-
|
54
|
+
writer_class = Avromatic.use_custom_datum_writer ? Avromatic::IO::DatumWriter : Avro::IO::DatumWriter
|
55
|
+
writer = writer_class.new(schema)
|
54
56
|
|
55
57
|
# The actual message comes last.
|
56
58
|
writer.write(message, encoder)
|
@@ -74,23 +74,23 @@ module Avromatic
|
|
74
74
|
def initialize(data = {})
|
75
75
|
super()
|
76
76
|
|
77
|
-
|
77
|
+
num_valid_keys = 0
|
78
78
|
attribute_definitions.each do |attribute_name, attribute_definition|
|
79
79
|
if data.include?(attribute_name)
|
80
|
-
|
80
|
+
num_valid_keys += 1
|
81
81
|
value = data.fetch(attribute_name)
|
82
82
|
send(attribute_definition.setter_name, value)
|
83
83
|
elsif data.include?(attribute_definition.name_string)
|
84
|
-
|
84
|
+
num_valid_keys += 1
|
85
85
|
value = data[attribute_definition.name_string]
|
86
86
|
send(attribute_definition.setter_name, value)
|
87
|
-
elsif !
|
87
|
+
elsif !_attributes.include?(attribute_name)
|
88
88
|
send(attribute_definition.setter_name, attribute_definition.default)
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
92
|
-
unless Avromatic.allow_unknown_attributes ||
|
93
|
-
unknown_attributes = (data.keys.map(&:to_s) -
|
92
|
+
unless Avromatic.allow_unknown_attributes || num_valid_keys == data.size
|
93
|
+
unknown_attributes = (data.keys.map(&:to_s) - _attributes.keys.map(&:to_s)).sort
|
94
94
|
allowed_attributes = attribute_definitions.keys.map(&:to_s).sort
|
95
95
|
message = "Unexpected arguments for #{self.class.name}#initialize: #{unknown_attributes.join(', ')}. " \
|
96
96
|
"Only the following arguments are allowed: #{allowed_attributes.join(', ')}. Provided arguments: #{data.inspect}"
|
@@ -8,6 +8,17 @@ module Avromatic
|
|
8
8
|
module Configurable
|
9
9
|
extend ActiveSupport::Concern
|
10
10
|
|
11
|
+
# Wraps a reference to a field so we can access both the string and symbolized versions of the name
|
12
|
+
# without repeated memory allocations.
|
13
|
+
class FieldReference
|
14
|
+
attr_reader :name, :name_sym
|
15
|
+
|
16
|
+
def initialize(name)
|
17
|
+
@name = -name
|
18
|
+
@name_sym = name.to_sym
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
11
22
|
module ClassMethods
|
12
23
|
attr_accessor :config
|
13
24
|
delegate :avro_schema, :value_avro_schema, :key_avro_schema, to: :config
|
@@ -20,6 +31,18 @@ module Avromatic
|
|
20
31
|
@key_avro_field_names ||= key_avro_schema.fields.map(&:name).map(&:to_sym).freeze
|
21
32
|
end
|
22
33
|
|
34
|
+
def value_avro_field_references
|
35
|
+
@value_avro_field_references ||= value_avro_schema.fields.map do |field|
|
36
|
+
Avromatic::Model::Configurable::FieldReference.new(field.name)
|
37
|
+
end.freeze
|
38
|
+
end
|
39
|
+
|
40
|
+
def key_avro_field_references
|
41
|
+
@key_avro_field_references ||= key_avro_schema.fields.map do |field|
|
42
|
+
Avromatic::Model::Configurable::FieldReference.new(field.name)
|
43
|
+
end.freeze
|
44
|
+
end
|
45
|
+
|
23
46
|
def value_avro_fields_by_name
|
24
47
|
@value_avro_fields_by_name ||= mapped_by_name(value_avro_schema)
|
25
48
|
end
|
@@ -43,6 +66,7 @@ module Avromatic
|
|
43
66
|
|
44
67
|
delegate :avro_schema, :value_avro_schema, :key_avro_schema,
|
45
68
|
:value_avro_field_names, :key_avro_field_names,
|
69
|
+
:value_avro_field_references, :key_avro_field_references,
|
46
70
|
to: :class
|
47
71
|
end
|
48
72
|
end
|
@@ -48,7 +48,8 @@ module Avromatic
|
|
48
48
|
value_attributes = avro_messaging
|
49
49
|
.decode(message_value, schema_name: avro_schema.fullname)
|
50
50
|
|
51
|
-
value_attributes.merge!(key_attributes
|
51
|
+
value_attributes.merge!(key_attributes) if key_attributes
|
52
|
+
value_attributes
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
@@ -29,34 +29,37 @@ module Avromatic
|
|
29
29
|
|
30
30
|
def value_attributes_for_avro(validate: true)
|
31
31
|
if self.class.config.mutable
|
32
|
-
avro_hash(
|
32
|
+
avro_hash(value_avro_field_references, validate: validate)
|
33
33
|
else
|
34
|
-
@value_attributes_for_avro ||= avro_hash(
|
34
|
+
@value_attributes_for_avro ||= avro_hash(value_avro_field_references, validate: validate)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
38
|
def key_attributes_for_avro(validate: true)
|
39
|
-
avro_hash(
|
39
|
+
avro_hash(key_avro_field_references, validate: validate)
|
40
40
|
end
|
41
41
|
|
42
42
|
def avro_value_datum(validate: true)
|
43
43
|
if self.class.config.mutable
|
44
|
-
avro_hash(
|
44
|
+
avro_hash(value_avro_field_references, strict: true, validate: validate)
|
45
45
|
else
|
46
|
-
@avro_datum ||= avro_hash(
|
46
|
+
@avro_datum ||= avro_hash(value_avro_field_references, strict: true, validate: validate)
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
50
|
def avro_key_datum(validate: true)
|
51
|
-
avro_hash(
|
51
|
+
avro_hash(key_avro_field_references, strict: true, validate: validate)
|
52
52
|
end
|
53
53
|
|
54
54
|
private
|
55
55
|
|
56
|
-
def avro_hash(
|
56
|
+
def avro_hash(field_references, strict: false, validate:)
|
57
57
|
avro_validate! if validate
|
58
|
-
|
59
|
-
|
58
|
+
field_references.each_with_object(Hash.new) do |field_reference, result|
|
59
|
+
next unless _attributes.include?(field_reference.name_sym)
|
60
|
+
|
61
|
+
value = _attributes[field_reference.name_sym]
|
62
|
+
result[field_reference.name] = attribute_definitions[field_reference.name_sym].serialize(value, strict)
|
60
63
|
end
|
61
64
|
end
|
62
65
|
|
@@ -78,8 +81,8 @@ module Avromatic
|
|
78
81
|
def avro_raw_decode(key: nil, value:, key_schema: nil, value_schema: nil)
|
79
82
|
key_attributes = key && decode_avro_datum(key, key_schema, :key)
|
80
83
|
value_attributes = decode_avro_datum(value, value_schema, :value)
|
81
|
-
|
82
|
-
new(value_attributes
|
84
|
+
value_attributes.merge!(key_attributes) if key_attributes
|
85
|
+
new(value_attributes)
|
83
86
|
end
|
84
87
|
|
85
88
|
private
|
@@ -31,7 +31,9 @@ module Avromatic
|
|
31
31
|
raise "#{__method__} must be overridden by #{self.class.name}"
|
32
32
|
end
|
33
33
|
|
34
|
-
|
34
|
+
# Note we use positional args rather than keyword args to reduce
|
35
|
+
# memory allocations
|
36
|
+
def serialize(_value, _strict)
|
35
37
|
raise "#{__method__} must be overridden by #{self.class.name}"
|
36
38
|
end
|
37
39
|
|
@@ -40,11 +40,11 @@ module Avromatic
|
|
40
40
|
value.nil? || (value.is_a?(::Array) && value.all? { |element| value_type.coerced?(element) })
|
41
41
|
end
|
42
42
|
|
43
|
-
def serialize(value, strict
|
43
|
+
def serialize(value, strict)
|
44
44
|
if value.nil?
|
45
45
|
value
|
46
46
|
else
|
47
|
-
value.map { |element| value_type.serialize(element, strict
|
47
|
+
value.map { |element| value_type.serialize(element, strict) }
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
@@ -59,12 +59,12 @@ module Avromatic
|
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
|
-
def serialize(value, strict
|
62
|
+
def serialize(value, strict)
|
63
63
|
if value.nil?
|
64
64
|
value
|
65
65
|
else
|
66
66
|
value.each_with_object({}) do |(element_key, element_value), result|
|
67
|
-
result[key_type.serialize(element_key, strict
|
67
|
+
result[key_type.serialize(element_key, strict)] = value_type.serialize(element_value, strict)
|
68
68
|
end
|
69
69
|
end
|
70
70
|
end
|
@@ -39,13 +39,9 @@ module Avromatic
|
|
39
39
|
value.nil? || value.is_a?(record_class)
|
40
40
|
end
|
41
41
|
|
42
|
-
def serialize(value, strict
|
42
|
+
def serialize(value, strict)
|
43
43
|
if value.nil?
|
44
44
|
value
|
45
|
-
elsif !strict && Avromatic.use_custom_datum_writer && Avromatic.use_encoding_providers? && !record_class.config.mutable
|
46
|
-
# n.b. Ideally we'd just return value here instead of wrapping it in a
|
47
|
-
# hash but then we'd have no place to stash the union member index...
|
48
|
-
{ Avromatic::IO::ENCODING_PROVIDER => value }
|
49
45
|
else
|
50
46
|
# This is only used for recursive serialization so validation has already been done
|
51
47
|
strict ? value.avro_value_datum(validate: false) : value.value_attributes_for_avro(validate: false)
|
@@ -13,6 +13,10 @@ module Avromatic
|
|
13
13
|
'timestamp-micros'
|
14
14
|
end
|
15
15
|
|
16
|
+
def referenced_model_classes
|
17
|
+
EMPTY_ARRAY
|
18
|
+
end
|
19
|
+
|
16
20
|
private
|
17
21
|
|
18
22
|
def truncated?(value)
|
@@ -25,10 +29,6 @@ module Avromatic
|
|
25
29
|
# of time zone.
|
26
30
|
::Time.at(input.to_i, input.usec)
|
27
31
|
end
|
28
|
-
|
29
|
-
def referenced_model_classes
|
30
|
-
EMPTY_ARRAY
|
31
|
-
end
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
@@ -13,6 +13,10 @@ module Avromatic
|
|
13
13
|
'timestamp-millis'
|
14
14
|
end
|
15
15
|
|
16
|
+
def referenced_model_classes
|
17
|
+
EMPTY_ARRAY
|
18
|
+
end
|
19
|
+
|
16
20
|
private
|
17
21
|
|
18
22
|
def truncated?(value)
|
@@ -25,10 +29,6 @@ module Avromatic
|
|
25
29
|
# of time zone.
|
26
30
|
::Time.at(input.to_i, input.usec / 1000 * 1000)
|
27
31
|
end
|
28
|
-
|
29
|
-
def referenced_model_classes
|
30
|
-
EMPTY_ARRAY
|
31
|
-
end
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
@@ -7,7 +7,6 @@ module Avromatic
|
|
7
7
|
module Model
|
8
8
|
module Types
|
9
9
|
class UnionType < AbstractType
|
10
|
-
MEMBER_INDEX = ::Avromatic::IO::DatumReader::UNION_MEMBER_INDEX
|
11
10
|
attr_reader :member_types, :value_classes, :input_classes
|
12
11
|
|
13
12
|
def initialize(member_types:)
|
@@ -24,8 +23,8 @@ module Avromatic
|
|
24
23
|
return input if coerced?(input)
|
25
24
|
|
26
25
|
result = nil
|
27
|
-
if input.is_a?(
|
28
|
-
result = member_types[input.
|
26
|
+
if input.is_a?(Avromatic::IO::UnionDatum)
|
27
|
+
result = member_types[input.member_index].coerce(input.datum)
|
29
28
|
else
|
30
29
|
member_types.find do |member_type|
|
31
30
|
result = safe_coerce(member_type, input)
|
@@ -40,18 +39,22 @@ module Avromatic
|
|
40
39
|
end
|
41
40
|
|
42
41
|
def coerced?(value)
|
42
|
+
return false if value.is_a?(Avromatic::IO::UnionDatum)
|
43
|
+
|
43
44
|
value.nil? || member_types.any? do |member_type|
|
44
45
|
member_type.coerced?(value)
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
48
49
|
def coercible?(input)
|
50
|
+
return true if value.is_a?(Avromatic::IO::UnionDatum)
|
51
|
+
|
49
52
|
coerced?(input) || member_types.any? do |member_type|
|
50
53
|
member_type.coercible?(input)
|
51
54
|
end
|
52
55
|
end
|
53
56
|
|
54
|
-
def serialize(value, strict
|
57
|
+
def serialize(value, strict)
|
55
58
|
# Avromatic does not treat the null of an optional field as part of the union
|
56
59
|
return nil if value.nil?
|
57
60
|
|
@@ -60,11 +63,11 @@ module Avromatic
|
|
60
63
|
raise ArgumentError.new("Expected #{value.inspect} to be one of #{value_classes.map(&:name)}")
|
61
64
|
end
|
62
65
|
|
63
|
-
|
64
|
-
if !strict && Avromatic.use_custom_datum_writer
|
65
|
-
|
66
|
+
serialized_value = member_types[member_index].serialize(value, strict)
|
67
|
+
if !strict && Avromatic.use_custom_datum_writer
|
68
|
+
serialized_value = Avromatic::IO::UnionDatum.new(member_index, serialized_value)
|
66
69
|
end
|
67
|
-
|
70
|
+
serialized_value
|
68
71
|
end
|
69
72
|
|
70
73
|
def referenced_model_classes
|