avromatic 2.2.6 → 3.0.2

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.
@@ -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,6 +80,8 @@ 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 = {})
@@ -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 }
@@ -19,9 +19,12 @@ module Avromatic
19
19
  end
20
20
  end
21
21
 
22
+ included do
23
+ class_attribute :config, instance_accessor: false, instance_predicate: false
24
+ end
25
+
22
26
  module ClassMethods
23
- attr_accessor :config
24
- 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
25
28
 
26
29
  def value_avro_field_names
27
30
  @value_avro_field_names ||= value_avro_schema.fields.map(&:name).map(&:to_sym).freeze
@@ -67,6 +70,7 @@ module Avromatic
67
70
  delegate :avro_schema, :value_avro_schema, :key_avro_schema,
68
71
  :value_avro_field_names, :key_avro_field_names,
69
72
  :value_avro_field_references, :key_avro_field_references,
73
+ :mutable?, :immutable?,
70
74
  to: :class
71
75
  end
72
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
 
@@ -14,12 +14,14 @@ module Avromatic
14
14
  def register!
15
15
  return unless key_avro_schema.nil? && value_avro_schema.type_sym == :record
16
16
 
17
+ processed = Set.new
17
18
  roots = [self]
18
19
  until roots.empty?
19
20
  model = roots.shift
20
- next if nested_models.registered?(model)
21
+ # Avoid any nested model dependency cycles by ignoring already processed models
22
+ next unless processed.add?(model)
21
23
 
22
- nested_models.register(model)
24
+ nested_models.ensure_registered_model(model)
23
25
  roots.concat(model.referenced_model_classes)
24
26
  end
25
27
  end
@@ -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,55 +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_references, 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_references, 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_references, 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_references, 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_references, 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_references, 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(field_references, strict: false, validate:)
57
- avro_validate! if validate
84
+ def avro_hash(field_references, strict: false)
58
85
  field_references.each_with_object(Hash.new) do |field_reference, result|
59
- next unless _attributes.include?(field_reference.name_sym)
60
-
86
+ attribute_definition = self.class.attribute_definitions[field_reference.name_sym]
61
87
  value = _attributes[field_reference.name_sym]
62
- result[field_reference.name] = attribute_definitions[field_reference.name_sym].serialize(value, strict)
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
63
103
  end
64
104
  end
65
105
 
@@ -81,8 +121,8 @@ module Avromatic
81
121
  def avro_raw_decode(key: nil, value:, key_schema: nil, value_schema: nil)
82
122
  key_attributes = key && decode_avro_datum(key, key_schema, :key)
83
123
  value_attributes = decode_avro_datum(value, value_schema, :value)
84
-
85
- new(value_attributes.merge!(key_attributes || {}))
124
+ value_attributes.merge!(key_attributes) if key_attributes
125
+ new(value_attributes)
86
126
  end
87
127
 
88
128
  private
@@ -42,13 +42,10 @@ module Avromatic
42
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 }
45
+ elsif strict
46
+ value.avro_value_datum
49
47
  else
50
- # This is only used for recursive serialization so validation has already been done
51
- strict ? value.avro_value_datum(validate: false) : value.value_attributes_for_avro(validate: false)
48
+ value.value_attributes_for_avro
52
49
  end
53
50
  end
54
51
 
@@ -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,15 +23,15 @@ 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)
32
31
  end
33
32
  end
34
33
 
35
- unless result
34
+ if result.nil?
36
35
  raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
37
36
  end
38
37
 
@@ -40,12 +39,16 @@ 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
@@ -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)
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