avromatic 2.2.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +89 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +4 -1
  5. data/Appraisals +8 -14
  6. data/CHANGELOG.md +23 -0
  7. data/README.md +3 -20
  8. data/avromatic.gemspec +9 -8
  9. data/bin/console +4 -3
  10. data/gemfiles/avro1_10_rails6_1.gemfile +9 -0
  11. data/gemfiles/{avro1_8_rails5_2.gemfile → avro1_9_rails6_1.gemfile} +3 -3
  12. data/lib/avromatic.rb +0 -5
  13. data/lib/avromatic/io.rb +1 -7
  14. data/lib/avromatic/io/datum_reader.rb +18 -68
  15. data/lib/avromatic/io/datum_writer.rb +5 -11
  16. data/lib/avromatic/io/union_datum.rb +25 -0
  17. data/lib/avromatic/messaging.rb +4 -2
  18. data/lib/avromatic/model/attributes.rb +28 -9
  19. data/lib/avromatic/model/configurable.rb +30 -2
  20. data/lib/avromatic/model/configuration.rb +5 -0
  21. data/lib/avromatic/model/field_helper.rb +5 -1
  22. data/lib/avromatic/model/messaging_serialization.rb +2 -1
  23. data/lib/avromatic/model/raw_serialization.rb +67 -24
  24. data/lib/avromatic/model/types/abstract_timestamp_type.rb +1 -1
  25. data/lib/avromatic/model/types/abstract_type.rb +3 -1
  26. data/lib/avromatic/model/types/array_type.rb +2 -2
  27. data/lib/avromatic/model/types/boolean_type.rb +1 -1
  28. data/lib/avromatic/model/types/custom_type.rb +1 -1
  29. data/lib/avromatic/model/types/date_type.rb +1 -1
  30. data/lib/avromatic/model/types/enum_type.rb +1 -1
  31. data/lib/avromatic/model/types/fixed_type.rb +1 -1
  32. data/lib/avromatic/model/types/float_type.rb +1 -1
  33. data/lib/avromatic/model/types/integer_type.rb +1 -1
  34. data/lib/avromatic/model/types/map_type.rb +2 -2
  35. data/lib/avromatic/model/types/null_type.rb +1 -1
  36. data/lib/avromatic/model/types/record_type.rb +4 -7
  37. data/lib/avromatic/model/types/string_type.rb +1 -1
  38. data/lib/avromatic/model/types/union_type.rb +12 -9
  39. data/lib/avromatic/model/validation.rb +2 -2
  40. data/lib/avromatic/version.rb +1 -1
  41. metadata +45 -34
  42. data/.travis.yml +0 -16
  43. data/gemfiles/avro_patches_rails5_2.gemfile +0 -9
  44. data/gemfiles/avro_patches_rails6_0.gemfile +0 -9
  45. data/lib/avromatic/patches.rb +0 -18
  46. 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)
@@ -27,7 +27,10 @@ module Avromatic
27
27
  def initialize(owner:, field:, type:)
28
28
  @owner = owner
29
29
  @field = field
30
+ @required = FieldHelper.required?(field)
31
+ @nullable = FieldHelper.nullable?(field)
30
32
  @type = type
33
+ @values_immutable = type.referenced_model_classes.all?(&:recursively_immutable?)
31
34
  @name = field.name.to_sym
32
35
  @name_string = field.name.to_s.dup.freeze
33
36
  @setter_name = "#{field.name}=".to_sym
@@ -40,8 +43,16 @@ module Avromatic
40
43
  end
41
44
  end
42
45
 
46
+ def nullable?
47
+ @nullable
48
+ end
49
+
43
50
  def required?
44
- FieldHelper.required?(field)
51
+ @required
52
+ end
53
+
54
+ def values_immutable?
55
+ @values_immutable
45
56
  end
46
57
 
47
58
  def coerce(input)
@@ -69,28 +80,30 @@ module Avromatic
69
80
  included do
70
81
  class_attribute :attribute_definitions, instance_writer: false
71
82
  self.attribute_definitions = {}
83
+
84
+ delegate :recursively_immutable?, to: :class
72
85
  end
73
86
 
74
87
  def initialize(data = {})
75
88
  super()
76
89
 
77
- valid_keys = []
90
+ num_valid_keys = 0
78
91
  attribute_definitions.each do |attribute_name, attribute_definition|
79
92
  if data.include?(attribute_name)
80
- valid_keys << attribute_name
93
+ num_valid_keys += 1
81
94
  value = data.fetch(attribute_name)
82
95
  send(attribute_definition.setter_name, value)
83
96
  elsif data.include?(attribute_definition.name_string)
84
- valid_keys << attribute_name
97
+ num_valid_keys += 1
85
98
  value = data[attribute_definition.name_string]
86
99
  send(attribute_definition.setter_name, value)
87
- elsif !attributes.include?(attribute_name)
100
+ elsif !_attributes.include?(attribute_name)
88
101
  send(attribute_definition.setter_name, attribute_definition.default)
89
102
  end
90
103
  end
91
104
 
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
105
+ unless Avromatic.allow_unknown_attributes || num_valid_keys == data.size
106
+ unknown_attributes = (data.keys.map(&:to_s) - _attributes.keys.map(&:to_s)).sort
94
107
  allowed_attributes = attribute_definitions.keys.map(&:to_s).sort
95
108
  message = "Unexpected arguments for #{self.class.name}#initialize: #{unknown_attributes.join(', ')}. " \
96
109
  "Only the following arguments are allowed: #{allowed_attributes.join(', ')}. Provided arguments: #{data.inspect}"
@@ -130,6 +143,12 @@ module Avromatic
130
143
  define_avro_attributes(avro_schema, generated_methods_module)
131
144
  end
132
145
 
146
+ def recursively_immutable?
147
+ return @recursively_immutable if defined?(@recursively_immutable)
148
+
149
+ @recursively_immutable = immutable? && attribute_definitions.each_value.all?(&:values_immutable?)
150
+ end
151
+
133
152
  private
134
153
 
135
154
  def check_for_field_conflicts!
@@ -174,10 +193,10 @@ module Avromatic
174
193
  generated_methods_module.send(:define_method, "#{field.name}?") { !!_attributes[symbolized_field_name] } if FieldHelper.boolean?(field)
175
194
 
176
195
  generated_methods_module.send(:define_method, "#{field.name}=") do |value|
177
- _attributes[symbolized_field_name] = attribute_definitions[symbolized_field_name].coerce(value)
196
+ _attributes[symbolized_field_name] = attribute_definition.coerce(value)
178
197
  end
179
198
 
180
- unless config.mutable # rubocop:disable Style/Next
199
+ unless mutable? # rubocop:disable Style/Next
181
200
  generated_methods_module.send(:private, "#{field.name}=")
182
201
  generated_methods_module.send(:define_method, :clone) { self }
183
202
  generated_methods_module.send(:define_method, :dup) { self }
@@ -8,9 +8,23 @@ 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
+
22
+ included do
23
+ class_attribute :config, instance_accessor: false, instance_predicate: false
24
+ end
25
+
11
26
  module ClassMethods
12
- attr_accessor :config
13
- delegate :avro_schema, :value_avro_schema, :key_avro_schema, to: :config
27
+ delegate :avro_schema, :value_avro_schema, :key_avro_schema, :mutable?, :immutable?, to: :config
14
28
 
15
29
  def value_avro_field_names
16
30
  @value_avro_field_names ||= value_avro_schema.fields.map(&:name).map(&:to_sym).freeze
@@ -20,6 +34,18 @@ module Avromatic
20
34
  @key_avro_field_names ||= key_avro_schema.fields.map(&:name).map(&:to_sym).freeze
21
35
  end
22
36
 
37
+ def value_avro_field_references
38
+ @value_avro_field_references ||= value_avro_schema.fields.map do |field|
39
+ Avromatic::Model::Configurable::FieldReference.new(field.name)
40
+ end.freeze
41
+ end
42
+
43
+ def key_avro_field_references
44
+ @key_avro_field_references ||= key_avro_schema.fields.map do |field|
45
+ Avromatic::Model::Configurable::FieldReference.new(field.name)
46
+ end.freeze
47
+ end
48
+
23
49
  def value_avro_fields_by_name
24
50
  @value_avro_fields_by_name ||= mapped_by_name(value_avro_schema)
25
51
  end
@@ -43,6 +69,8 @@ module Avromatic
43
69
 
44
70
  delegate :avro_schema, :value_avro_schema, :key_avro_schema,
45
71
  :value_avro_field_names, :key_avro_field_names,
72
+ :value_avro_field_references, :key_avro_field_references,
73
+ :mutable?, :immutable?,
46
74
  to: :class
47
75
  end
48
76
  end
@@ -8,6 +8,7 @@ module Avromatic
8
8
 
9
9
  attr_reader :avro_schema, :key_avro_schema, :nested_models, :mutable,
10
10
  :allow_optional_key_fields
11
+ alias_method :mutable?, :mutable
11
12
  delegate :schema_store, to: Avromatic
12
13
 
13
14
  # Either schema(_name) or value_schema(_name), but not both, must be
@@ -34,6 +35,10 @@ module Avromatic
34
35
 
35
36
  alias_method :value_avro_schema, :avro_schema
36
37
 
38
+ def immutable?
39
+ !mutable?
40
+ end
41
+
37
42
  private
38
43
 
39
44
  def find_avro_schema(**options)
@@ -16,9 +16,13 @@ module Avromatic
16
16
  !optional?(field)
17
17
  end
18
18
 
19
+ def nullable?(field)
20
+ optional?(field) || field.type.type_sym == :null
21
+ end
22
+
19
23
  def boolean?(field)
20
24
  field.type.type_sym == :boolean ||
21
- (FieldHelper.optional?(field) && field.type.schemas.last.type_sym == :boolean)
25
+ (optional?(field) && field.type.schemas.last.type_sym == :boolean)
22
26
  end
23
27
  end
24
28
  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
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/deprecation'
4
+
3
5
  module Avromatic
4
6
  module Model
5
7
 
@@ -11,52 +13,93 @@ module Avromatic
11
13
  module Encode
12
14
  extend ActiveSupport::Concern
13
15
 
16
+ UNSPECIFIED = Object.new
17
+
14
18
  delegate :datum_writer, :datum_reader, to: :class
15
19
  private :datum_writer, :datum_reader
16
20
 
17
- def avro_raw_value(validate: true)
18
- if self.class.config.mutable
19
- avro_raw_encode(value_attributes_for_avro(validate: validate), :value)
21
+ def avro_raw_value(validate: UNSPECIFIED)
22
+ unless validate == UNSPECIFIED
23
+ ActiveSupport::Deprecation.warn("The 'validate' argument to #{__method__} is deprecated.")
24
+ end
25
+
26
+ if self.class.recursively_immutable?
27
+ @avro_raw_value ||= avro_raw_encode(value_attributes_for_avro, :value)
20
28
  else
21
- @avro_raw_value ||= avro_raw_encode(value_attributes_for_avro(validate: validate), :value)
29
+ avro_raw_encode(value_attributes_for_avro, :value)
22
30
  end
23
31
  end
24
32
 
25
- def avro_raw_key(validate: true)
33
+ def avro_raw_key(validate: UNSPECIFIED)
34
+ unless validate == UNSPECIFIED
35
+ ActiveSupport::Deprecation.warn("The 'validate' argument to #{__method__} is deprecated.")
36
+ end
37
+
26
38
  raise 'Model has no key schema' unless key_avro_schema
27
- avro_raw_encode(key_attributes_for_avro(validate: validate), :key)
39
+ avro_raw_encode(key_attributes_for_avro, :key)
28
40
  end
29
41
 
30
- def value_attributes_for_avro(validate: true)
31
- if self.class.config.mutable
32
- avro_hash(value_avro_field_names, validate: validate)
42
+ def value_attributes_for_avro(validate: UNSPECIFIED)
43
+ unless validate == UNSPECIFIED
44
+ ActiveSupport::Deprecation.warn("The 'validate' argument to #{__method__} is deprecated.")
45
+ end
46
+
47
+ if self.class.recursively_immutable?
48
+ @value_attributes_for_avro ||= avro_hash(value_avro_field_references)
33
49
  else
34
- @value_attributes_for_avro ||= avro_hash(value_avro_field_names, validate: validate)
50
+ avro_hash(value_avro_field_references)
35
51
  end
36
52
  end
37
53
 
38
- def key_attributes_for_avro(validate: true)
39
- avro_hash(key_avro_field_names, validate: validate)
54
+ def key_attributes_for_avro(validate: UNSPECIFIED)
55
+ unless validate == UNSPECIFIED
56
+ ActiveSupport::Deprecation.warn("The 'validate' argument to #{__method__} is deprecated.")
57
+ end
58
+
59
+ avro_hash(key_avro_field_references)
40
60
  end
41
61
 
42
- def avro_value_datum(validate: true)
43
- if self.class.config.mutable
44
- avro_hash(value_avro_field_names, strict: true, validate: validate)
62
+ def avro_value_datum(validate: UNSPECIFIED)
63
+ unless validate == UNSPECIFIED
64
+ ActiveSupport::Deprecation.warn("The 'validate' argument to #{__method__} is deprecated.")
65
+ end
66
+
67
+ if self.class.recursively_immutable?
68
+ @avro_value_datum ||= avro_hash(value_avro_field_references, strict: true)
45
69
  else
46
- @avro_datum ||= avro_hash(value_avro_field_names, strict: true, validate: validate)
70
+ avro_hash(value_avro_field_references, strict: true)
47
71
  end
48
72
  end
49
73
 
50
- def avro_key_datum(validate: true)
51
- avro_hash(key_avro_field_names, strict: true, validate: validate)
74
+ def avro_key_datum(validate: UNSPECIFIED)
75
+ unless validate == UNSPECIFIED
76
+ ActiveSupport::Deprecation.warn("The 'validate' argument to #{__method__} is deprecated.")
77
+ end
78
+
79
+ avro_hash(key_avro_field_references, strict: true)
52
80
  end
53
81
 
54
82
  private
55
83
 
56
- def avro_hash(fields, strict: false, validate:)
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)
84
+ def avro_hash(field_references, strict: false)
85
+ field_references.each_with_object(Hash.new) do |field_reference, result|
86
+ attribute_definition = self.class.attribute_definitions[field_reference.name_sym]
87
+ value = _attributes[field_reference.name_sym]
88
+
89
+ if value.nil? && !attribute_definition.nullable?
90
+ # We're missing a required attribute so perform an explicit validation to generate
91
+ # a more complete error message
92
+ avro_validate!
93
+ elsif _attributes.include?(field_reference.name_sym)
94
+ begin
95
+ result[field_reference.name] = attribute_definition.serialize(value, strict)
96
+ rescue Avromatic::Model::ValidationError
97
+ # Perform an explicit validation to generate a more complete error message
98
+ avro_validate!
99
+ # We should never get here but just in case...
100
+ raise
101
+ end
102
+ end
60
103
  end
61
104
  end
62
105
 
@@ -78,8 +121,8 @@ module Avromatic
78
121
  def avro_raw_decode(key: nil, value:, key_schema: nil, value_schema: nil)
79
122
  key_attributes = key && decode_avro_datum(key, key_schema, :key)
80
123
  value_attributes = decode_avro_datum(value, value_schema, :value)
81
-
82
- new(value_attributes.merge!(key_attributes || {}))
124
+ value_attributes.merge!(key_attributes) if key_attributes
125
+ new(value_attributes)
83
126
  end
84
127
 
85
128
  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