avromatic 2.2.2 → 2.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +85 -0
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -1
  5. data/Appraisals +34 -22
  6. data/CHANGELOG.md +16 -0
  7. data/README.md +3 -3
  8. data/avromatic.gemspec +7 -6
  9. data/bin/console +4 -3
  10. data/gemfiles/{rails5_0.gemfile → avro1_10_rails5_2.gemfile} +3 -3
  11. data/gemfiles/avro1_10_rails6_0.gemfile +9 -0
  12. data/gemfiles/avro1_10_rails6_1.gemfile +9 -0
  13. data/gemfiles/{rails5_2.gemfile → avro1_8_rails5_2.gemfile} +0 -0
  14. data/gemfiles/{rails5_1.gemfile → avro1_9_rails5_2.gemfile} +3 -3
  15. data/gemfiles/{rails6_0.gemfile → avro1_9_rails6_0.gemfile} +1 -1
  16. data/gemfiles/{avro_patches_rails5_0.gemfile → avro1_9_rails6_1.gemfile} +3 -3
  17. data/gemfiles/avro_patches_rails5_2.gemfile +1 -1
  18. data/gemfiles/avro_patches_rails6_1.gemfile +9 -0
  19. data/lib/avromatic.rb +0 -5
  20. data/lib/avromatic/io.rb +1 -7
  21. data/lib/avromatic/io/datum_reader.rb +18 -68
  22. data/lib/avromatic/io/datum_writer.rb +5 -11
  23. data/lib/avromatic/io/union_datum.rb +25 -0
  24. data/lib/avromatic/messaging.rb +4 -2
  25. data/lib/avromatic/model/attributes.rb +6 -6
  26. data/lib/avromatic/model/configurable.rb +24 -0
  27. data/lib/avromatic/model/messaging_serialization.rb +2 -1
  28. data/lib/avromatic/model/raw_serialization.rb +14 -11
  29. data/lib/avromatic/model/types/abstract_timestamp_type.rb +1 -1
  30. data/lib/avromatic/model/types/abstract_type.rb +3 -1
  31. data/lib/avromatic/model/types/array_type.rb +2 -2
  32. data/lib/avromatic/model/types/boolean_type.rb +1 -1
  33. data/lib/avromatic/model/types/custom_type.rb +1 -1
  34. data/lib/avromatic/model/types/date_type.rb +1 -1
  35. data/lib/avromatic/model/types/enum_type.rb +1 -1
  36. data/lib/avromatic/model/types/fixed_type.rb +1 -1
  37. data/lib/avromatic/model/types/float_type.rb +1 -1
  38. data/lib/avromatic/model/types/integer_type.rb +1 -1
  39. data/lib/avromatic/model/types/map_type.rb +2 -2
  40. data/lib/avromatic/model/types/null_type.rb +1 -1
  41. data/lib/avromatic/model/types/record_type.rb +1 -5
  42. data/lib/avromatic/model/types/string_type.rb +1 -1
  43. data/lib/avromatic/model/types/timestamp_micros_type.rb +4 -4
  44. data/lib/avromatic/model/types/timestamp_millis_type.rb +4 -4
  45. data/lib/avromatic/model/types/union_type.rb +11 -8
  46. data/lib/avromatic/version.rb +1 -1
  47. metadata +62 -47
  48. data/.travis.yml +0 -23
  49. data/gemfiles/avro_patches_rails5_1.gemfile +0 -9
  50. data/lib/avromatic/patches.rb +0 -18
  51. 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?(Hash) && datum.key?(Avromatic::IO::UNION_MEMBER_INDEX)
11
- index_of_schema = datum[Avromatic::IO::UNION_MEMBER_INDEX]
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
@@ -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
- reader = Avromatic::IO::DatumReader.new(writers_schema, readers_schema)
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
- writer = Avromatic::IO::DatumWriter.new(schema)
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
- valid_keys = []
77
+ num_valid_keys = 0
78
78
  attribute_definitions.each do |attribute_name, attribute_definition|
79
79
  if data.include?(attribute_name)
80
- valid_keys << attribute_name
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
- valid_keys << attribute_name
84
+ num_valid_keys += 1
85
85
  value = data[attribute_definition.name_string]
86
86
  send(attribute_definition.setter_name, value)
87
- elsif !attributes.include?(attribute_name)
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 || valid_keys.size == data.size
93
- unknown_attributes = (data.keys.map(&:to_s) - valid_keys.map(&:to_s)).sort
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(value_avro_field_names, validate: validate)
32
+ avro_hash(value_avro_field_references, validate: validate)
33
33
  else
34
- @value_attributes_for_avro ||= avro_hash(value_avro_field_names, validate: validate)
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(key_avro_field_names, validate: validate)
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(value_avro_field_names, strict: true, validate: validate)
44
+ avro_hash(value_avro_field_references, strict: true, validate: validate)
45
45
  else
46
- @avro_datum ||= avro_hash(value_avro_field_names, strict: true, validate: validate)
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(key_avro_field_names, strict: true, validate: validate)
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(fields, strict: false, validate:)
56
+ def avro_hash(field_references, strict: false, validate:)
57
57
  avro_validate! if validate
58
- attributes.slice(*fields).each_with_object(Hash.new) do |(key, value), result|
59
- result[key.to_s] = attribute_definitions[key].serialize(value, strict: strict)
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.merge!(key_attributes || {}))
84
+ value_attributes.merge!(key_attributes) if key_attributes
85
+ new(value_attributes)
83
86
  end
84
87
 
85
88
  private
@@ -38,7 +38,7 @@ module Avromatic
38
38
  value.is_a?(::Time) && value.class != ActiveSupport::TimeWithZone && truncated?(value)
39
39
  end
40
40
 
41
- def serialize(value, **)
41
+ def serialize(value, _strict)
42
42
  value
43
43
  end
44
44
 
@@ -31,7 +31,9 @@ module Avromatic
31
31
  raise "#{__method__} must be overridden by #{self.class.name}"
32
32
  end
33
33
 
34
- def serialize(_value, **)
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: strict) }
47
+ value.map { |element| value_type.serialize(element, strict) }
48
48
  end
49
49
  end
50
50
 
@@ -30,7 +30,7 @@ module Avromatic
30
30
 
31
31
  alias_method :coerced?, :coercible?
32
32
 
33
- def serialize(value, **)
33
+ def serialize(value, _strict)
34
34
  value
35
35
  end
36
36
 
@@ -55,7 +55,7 @@ module Avromatic
55
55
  false
56
56
  end
57
57
 
58
- def serialize(value, **)
58
+ def serialize(value, _strict)
59
59
  @serializer.call(value)
60
60
  end
61
61
 
@@ -32,7 +32,7 @@ module Avromatic
32
32
 
33
33
  alias_method :coerced?, :coercible?
34
34
 
35
- def serialize(value, **)
35
+ def serialize(value, _strict)
36
36
  value
37
37
  end
38
38
 
@@ -47,7 +47,7 @@ module Avromatic
47
47
  (input.is_a?(::Symbol) && allowed_values.include?(input.to_s))
48
48
  end
49
49
 
50
- def serialize(value, **)
50
+ def serialize(value, _strict)
51
51
  value
52
52
  end
53
53
 
@@ -36,7 +36,7 @@ module Avromatic
36
36
 
37
37
  alias_method :coerced?, :coercible?
38
38
 
39
- def serialize(value, **)
39
+ def serialize(value, _strict)
40
40
  value
41
41
  end
42
42
 
@@ -39,7 +39,7 @@ module Avromatic
39
39
  input.nil? || input.is_a?(::Float)
40
40
  end
41
41
 
42
- def serialize(value, **)
42
+ def serialize(value, _strict)
43
43
  value
44
44
  end
45
45
 
@@ -30,7 +30,7 @@ module Avromatic
30
30
 
31
31
  alias_method :coerced?, :coercible?
32
32
 
33
- def serialize(value, **)
33
+ def serialize(value, _strict)
34
34
  value
35
35
  end
36
36
 
@@ -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: strict)] = value_type.serialize(element_value, strict: strict)
67
+ result[key_type.serialize(element_key, strict)] = value_type.serialize(element_value, strict)
68
68
  end
69
69
  end
70
70
  end
@@ -30,7 +30,7 @@ module Avromatic
30
30
 
31
31
  alias_method :coerced?, :coercible?
32
32
 
33
- def serialize(_value, **)
33
+ def serialize(_value, _strict)
34
34
  nil
35
35
  end
36
36
 
@@ -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)
@@ -39,7 +39,7 @@ module Avromatic
39
39
  value.nil? || value.is_a?(::String)
40
40
  end
41
41
 
42
- def serialize(value, **)
42
+ def serialize(value, _strict)
43
43
  value
44
44
  end
45
45
 
@@ -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?(Hash) && input.key?(MEMBER_INDEX)
28
- result = member_types[input.delete(MEMBER_INDEX)].coerce(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
- hash = member_types[member_index].serialize(value, strict: strict)
64
- if !strict && Avromatic.use_custom_datum_writer && value.is_a?(Avromatic::Model::Attributes)
65
- hash[Avromatic::IO::UNION_MEMBER_INDEX] = member_index
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
- hash
70
+ serialized_value
68
71
  end
69
72
 
70
73
  def referenced_model_classes