avromatic 2.2.6 → 2.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5038f6f83ba2ca87bc8cd16a96aae5528397ca5cf9ef01d955861459ae68391a
4
- data.tar.gz: 83f5e8bb53086297882f77108480976292a2ca97b40dcb3da5a23c579a7c1edd
3
+ metadata.gz: a7401c0e72992925f02234f5d3d2ab8e762a36bc62aa25c631569d6f4d9918d2
4
+ data.tar.gz: f2121b2a62bf60893ea0eb143ed83a142ea069d07839fb8121d0618483b469d5
5
5
  SHA512:
6
- metadata.gz: 5d5f8501fc5e050db26475ea1205a1c08f864d6fae8272e54c4a04df4fb22bf634868dadde7eade371a6b22f5438423f84a6405808286b1c687fb7acb26859c3
7
- data.tar.gz: 7025885a25ab59923d8efc08e494a51f3c6bc57f0e72eedc84c00b9b46c1ef1df085d99ba9135d6997a8370f3fb1497690a714ca067896faad1a2e9a25f0e970
6
+ metadata.gz: 42be85747686fca4ec66d6e178f7e5aab6d9e9ab88cd4d107ed1c5de8a83017f083a510889ced33a2eb284f45865b3c32b3a9edad200011aed5b17c284f3a593
7
+ data.tar.gz: be13122bca8693d33e20d1317ae5cd88d8c1c975c08d94ed522d0330d70f202bc43d79d7f2941032bc7540ad83b2d647d3c7d29bb5b72c03a560dfedb85e62f7
@@ -0,0 +1,85 @@
1
+ version: 2.1
2
+ jobs:
3
+ lint:
4
+ docker:
5
+ - image: salsify/ruby_ci:2.5.8
6
+ working_directory: ~/avromatic
7
+ steps:
8
+ - checkout
9
+ - restore_cache:
10
+ keys:
11
+ - v2-gems-ruby-2.5.8-{{ checksum "avromatic.gemspec" }}-{{ checksum "Gemfile" }}
12
+ - v2-gems-ruby-2.5.8-
13
+ - run:
14
+ name: Install Gems
15
+ command: |
16
+ if ! bundle check --path=vendor/bundle; then
17
+ bundle install --path=vendor/bundle --jobs=4 --retry=3
18
+ bundle clean
19
+ fi
20
+ - save_cache:
21
+ key: v2-gems-ruby-2.5.8-{{ checksum "avromatic.gemspec" }}-{{ checksum "Gemfile" }}
22
+ paths:
23
+ - "vendor/bundle"
24
+ - "gemfiles/vendor/bundle"
25
+ - run:
26
+ name: Run Rubocop
27
+ command: bundle exec rubocop
28
+ test:
29
+ parameters:
30
+ gemfile:
31
+ type: string
32
+ ruby-version:
33
+ type: string
34
+ docker:
35
+ - image: salsify/ruby_ci:<< parameters.ruby-version >>
36
+ environment:
37
+ CIRCLE_TEST_REPORTS: "test-results"
38
+ BUNDLE_GEMFILE: << parameters.gemfile >>
39
+ working_directory: ~/avromatic
40
+ steps:
41
+ - checkout
42
+ - restore_cache:
43
+ keys:
44
+ - v2-gems-ruby-<< parameters.ruby-version >>-{{ checksum "avromatic.gemspec" }}-{{ checksum "<< parameters.gemfile >>" }}
45
+ - v2-gems-ruby-<< parameters.ruby-version >>-
46
+ - run:
47
+ name: Install Gems
48
+ command: |
49
+ if ! bundle check --path=vendor/bundle; then
50
+ bundle install --path=vendor/bundle --jobs=4 --retry=3
51
+ bundle clean
52
+ fi
53
+ - save_cache:
54
+ key: v2-gems-ruby-<< parameters.ruby-version >>-{{ checksum "avromatic.gemspec" }}-{{ checksum "<< parameters.gemfile >>" }}
55
+ paths:
56
+ - "vendor/bundle"
57
+ - "gemfiles/vendor/bundle"
58
+ - run:
59
+ name: Run Tests
60
+ command: |
61
+ bundle exec rspec --format RspecJunitFormatter --out $CIRCLE_TEST_REPORTS/rspec/junit.xml --format progress spec
62
+ - store_test_results:
63
+ path: "test-results"
64
+ workflows:
65
+ build:
66
+ jobs:
67
+ - lint
68
+ - test:
69
+ matrix:
70
+ parameters:
71
+ gemfile:
72
+ - "gemfiles/avro1_8_rails5_2.gemfile"
73
+ - "gemfiles/avro1_9_rails5_2.gemfile"
74
+ - "gemfiles/avro1_10_rails5_2.gemfile"
75
+ - "gemfiles/avro1_9_rails6_0.gemfile"
76
+ - "gemfiles/avro1_10_rails6_0.gemfile"
77
+ - "gemfiles/avro_patches_rails5_2.gemfile"
78
+ - "gemfiles/avro_patches_rails6_0.gemfile"
79
+ - "gemfiles/avro1_10_rails6_1.gemfile"
80
+ - "gemfiles/avro1_9_rails6_1.gemfile"
81
+ - "gemfiles/avro_patches_rails6_1.gemfile"
82
+ ruby-version:
83
+ - "2.5.8"
84
+ - "2.6.6"
85
+ - "2.7.1"
@@ -3,6 +3,9 @@ inherit_gem:
3
3
 
4
4
  AllCops:
5
5
  TargetRubyVersion: 2.4
6
+ Exclude:
7
+ - 'vendor/**/*'
8
+ - 'gemfiles/vendor/**/*'
6
9
 
7
10
  Style/MultilineBlockChain:
8
11
  Enabled: false
data/Appraisals CHANGED
@@ -41,3 +41,21 @@ appraise 'avro-patches-rails6_0' do
41
41
  gem 'activesupport', '~> 6.0.0'
42
42
  gem 'activemodel', '~> 6.0.0'
43
43
  end
44
+
45
+ appraise 'avro1_9-rails6_1' do
46
+ gem 'avro', '1.9.2'
47
+ gem 'activesupport', '~> 6.1.0'
48
+ gem 'activemodel', '~> 6.1.0'
49
+ end
50
+
51
+ appraise 'avro1_10-rails6_1' do
52
+ gem 'avro', '~> 1.10.0'
53
+ gem 'activesupport', '~> 6.1.0'
54
+ gem 'activemodel', '~> 6.1.0'
55
+ end
56
+
57
+ appraise 'avro-patches-rails6_1' do
58
+ gem 'avro-patches', '>= 1.0.0'
59
+ gem 'activesupport', '~> 6.1.0'
60
+ gem 'activemodel', '~> 6.1.0'
61
+ end
@@ -1,5 +1,15 @@
1
1
  # avromatic changelog
2
2
 
3
+ ## v2.4.0
4
+ - Ignore the `validate` argument and always validate during serialization. This
5
+ argument will be removed in Avromatic 3.0.
6
+ - Optimize model validation during serialization.
7
+ - Don't cache immutable model validation results or serialized Avro attributes if a model has mutable children.
8
+
9
+ ## v2.3.0
10
+ - Add support for Rails 6.1.
11
+ - Optimize nested model serialization.
12
+
3
13
  ## v2.2.6
4
14
  - Optimize memory usage when serializing models.
5
15
 
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Avromatic
2
2
 
3
- [![Build Status](https://travis-ci.org/salsify/avromatic.svg?branch=master)][travis]
3
+ [![Build Status](https://circleci.com/gh/salsify/avromatic.svg?style=svg)][circleci]
4
4
  [![Gem Version](https://badge.fury.io/rb/avromatic.svg)](https://badge.fury.io/rb/avromatic)
5
5
 
6
- [travis]: http://travis-ci.org/salsify/avromatic
6
+ [circleci]: https://circleci.com/gh/salsify/avromatic
7
7
 
8
8
  `Avromatic` generates Ruby models from [Avro](http://avro.apache.org/) schemas
9
9
  and provides utilities to encode and decode them.
@@ -22,8 +22,8 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.required_ruby_version = '>= 2.4'
24
24
 
25
- spec.add_runtime_dependency 'activemodel', '>= 5.0', '< 6.1'
26
- spec.add_runtime_dependency 'activesupport', '>= 5.0', '< 6.1'
25
+ spec.add_runtime_dependency 'activemodel', '>= 5.0', '< 6.2'
26
+ spec.add_runtime_dependency 'activesupport', '>= 5.0', '< 6.2'
27
27
  spec.add_runtime_dependency 'avro', '>= 1.7.7', '< 1.11'
28
28
  spec.add_runtime_dependency 'avro_schema_registry-client', '>= 0.3.0'
29
29
  spec.add_runtime_dependency 'avro_turf'
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.add_development_dependency 'overcommit', '0.35.0'
36
36
  spec.add_development_dependency 'rake', '~> 10.0'
37
37
  spec.add_development_dependency 'rspec', '~> 3.0'
38
+ spec.add_development_dependency 'rspec_junit_formatter'
38
39
  spec.add_development_dependency 'salsify_rubocop', '~> 0.52.1.1'
39
40
  spec.add_development_dependency 'simplecov'
40
41
  spec.add_development_dependency 'webmock'
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "avromatic"
4
+ require 'bundler/setup'
5
+ require 'avromatic'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "avromatic"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "avro", "~> 1.10.0"
6
+ gem "activesupport", "~> 6.1.0"
7
+ gem "activemodel", "~> 6.1.0"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "avro", "1.9.2"
6
+ gem "activesupport", "~> 6.1.0"
7
+ gem "activemodel", "~> 6.1.0"
8
+
9
+ gemspec path: "../"
@@ -0,0 +1,9 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "avro-patches", ">= 1.0.0"
6
+ gem "activesupport", "~> 6.1.0"
7
+ gem "activemodel", "~> 6.1.0"
8
+
9
+ gemspec path: "../"
@@ -8,7 +8,6 @@ require 'avromatic/model'
8
8
  require 'avromatic/model_registry'
9
9
  require 'avromatic/messaging'
10
10
  require 'active_support/core_ext/string/inflections'
11
- require 'avromatic/patches'
12
11
 
13
12
  module Avromatic
14
13
  class << self
@@ -33,10 +32,6 @@ module Avromatic
33
32
  eager_load_models!
34
33
  end
35
34
 
36
- def self.use_encoding_providers?
37
- use_custom_datum_writer && defined?(Avromatic::Patches::SchemaValidatorPatch)
38
- end
39
-
40
35
  def self.build_schema_registry
41
36
  raise 'Avromatic must be configured with a registry_url' unless registry_url
42
37
  if use_schema_fingerprint_lookup
@@ -1,11 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Avromatic
4
- module IO
5
- UNION_MEMBER_INDEX = '__avromatic_member_index'
6
- ENCODING_PROVIDER = '__avromatic_encoding_provider'
7
- end
8
- end
9
-
10
3
  require 'avromatic/io/datum_reader'
11
4
  require 'avromatic/io/datum_writer'
5
+ require 'avromatic/io/union_datum'
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rubocop:disable Style/WhenThen
4
3
  module Avromatic
5
4
  module IO
6
5
  # Subclass DatumReader to include additional information about the union
@@ -8,81 +7,32 @@ module Avromatic
8
7
  # branch 'salsify-master' with the tag 'v1.9.0.3'
9
8
  class DatumReader < Avro::IO::DatumReader
10
9
 
11
- UNION_MEMBER_INDEX = Avromatic::IO::UNION_MEMBER_INDEX
12
-
13
- def read_data(writers_schema, readers_schema, decoder, initial_record = nil)
14
- # schema matching
15
- unless self.class.match_schemas(writers_schema, readers_schema)
16
- raise Avro::IO::SchemaMatchException.new(writers_schema, readers_schema)
17
- end
18
-
10
+ def read_data(writers_schema, readers_schema, decoder)
19
11
  # schema resolution: reader's schema is a union, writer's schema is not
20
- if writers_schema.type_sym != :union && readers_schema.type_sym == :union
21
- rs_index = readers_schema.schemas.find_index do |s|
22
- self.class.match_schemas(writers_schema, s)
23
- end
12
+ return super unless writers_schema.type_sym != :union && readers_schema.type_sym == :union
24
13
 
25
- optional = readers_schema.schemas.first.type_sym == :null
26
- union_info = if readers_schema.schemas.size == 2 && optional
27
- # Avromatic does not treat the union of null and 1 other type as a union
28
- nil
29
- elsif optional
30
- # Avromatic does not treat the null of an optional field as part of the union
31
- { UNION_MEMBER_INDEX => rs_index - 1 }
32
- else
33
- { UNION_MEMBER_INDEX => rs_index }
34
- end
35
-
36
- return read_data(writers_schema, readers_schema.schemas[rs_index], decoder, union_info) if rs_index
37
- raise Avro::IO::SchemaMatchException.new(writers_schema, readers_schema)
14
+ rs_index = readers_schema.schemas.find_index do |s|
15
+ self.class.match_schemas(writers_schema, s)
38
16
  end
39
17
 
40
- # function dispatch for reading data based on type of writer's schema
41
- datum = case writers_schema.type_sym
42
- when :null; decoder.read_null
43
- when :boolean; decoder.read_boolean
44
- when :string; decoder.read_string
45
- when :int; decoder.read_int
46
- when :long; decoder.read_long
47
- when :float; decoder.read_float
48
- when :double; decoder.read_double
49
- when :bytes; decoder.read_bytes
50
- when :fixed; read_fixed(writers_schema, readers_schema, decoder)
51
- when :enum; read_enum(writers_schema, readers_schema, decoder)
52
- when :array; read_array(writers_schema, readers_schema, decoder)
53
- when :map; read_map(writers_schema, readers_schema, decoder)
54
- when :union; read_union(writers_schema, readers_schema, decoder)
55
- when :record, :error, :request; read_record(writers_schema, readers_schema, decoder, initial_record || {})
56
- else
57
- raise Avro::AvroError.new("Cannot read unknown schema type: #{writers_schema.type}")
58
- end
18
+ raise Avro::IO::SchemaMatchException.new(writers_schema, readers_schema) unless rs_index
59
19
 
60
- # Allow this code to be used with an official Avro release or the
61
- # avro-patches gem that includes logical_type support.
62
- if readers_schema.respond_to?(:logical_type)
63
- readers_schema.type_adapter.decode(datum)
64
- else
65
- datum
66
- end
67
- end
20
+ datum = read_data(writers_schema, readers_schema.schemas[rs_index], decoder)
21
+ optional = readers_schema.schemas.first.type_sym == :null
68
22
 
69
- # Override to specify an initial record that may contain union index
70
- def read_record(writers_schema, readers_schema, decoder, initial_record = {})
71
- readers_fields_hash = readers_schema.fields_hash
72
- read_record = Avromatic.use_custom_datum_reader ? initial_record : {}
73
- writers_schema.fields.each do |field|
74
- readers_field = readers_fields_hash[field.name]
75
- if readers_field
76
- field_val = read_data(field.type, readers_field.type, decoder)
77
- read_record[field.name] = field_val
78
- else
79
- skip_data(field.type, decoder)
80
- end
23
+ if readers_schema.schemas.size == 2 && optional
24
+ # Avromatic does not treat the union of null and 1 other type as a union
25
+ datum
26
+ elsif datum.nil?
27
+ # Avromatic does not treat the null of an optional field as part of the union
28
+ nil
29
+ else
30
+ # Avromatic does not treat the null of an optional field as part of the union so
31
+ # adjust the member index accordingly
32
+ member_index = optional ? rs_index - 1 : rs_index
33
+ Avromatic::IO::UnionDatum.new(member_index, datum)
81
34
  end
82
-
83
- read_record
84
35
  end
85
36
  end
86
37
  end
87
38
  end
88
- # rubocop:enable Style/WhenThen
@@ -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
 
@@ -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,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,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
@@ -64,8 +64,8 @@ module Avromatic
64
64
  end
65
65
  end
66
66
 
67
- unless self.class.config.mutable
68
- @missing_attributes = missing_attributes.deep_freeze
67
+ if recursively_immutable?
68
+ @missing_attributes = missing_attributes.freeze
69
69
  end
70
70
 
71
71
  missing_attributes
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Avromatic
4
- VERSION = '2.2.6'
4
+ VERSION = '2.4.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: avromatic
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.6
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Salsify Engineering
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-07 00:00:00.000000000 Z
11
+ date: 2021-01-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: '5.0'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6.1'
22
+ version: '6.2'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: '5.0'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6.1'
32
+ version: '6.2'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: activesupport
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -39,7 +39,7 @@ dependencies:
39
39
  version: '5.0'
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '6.1'
42
+ version: '6.2'
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,7 +49,7 @@ dependencies:
49
49
  version: '5.0'
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '6.1'
52
+ version: '6.2'
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: avro
55
55
  requirement: !ruby/object:Gem::Requirement
@@ -196,6 +196,20 @@ dependencies:
196
196
  - - "~>"
197
197
  - !ruby/object:Gem::Version
198
198
  version: '3.0'
199
+ - !ruby/object:Gem::Dependency
200
+ name: rspec_junit_formatter
201
+ requirement: !ruby/object:Gem::Requirement
202
+ requirements:
203
+ - - ">="
204
+ - !ruby/object:Gem::Version
205
+ version: '0'
206
+ type: :development
207
+ prerelease: false
208
+ version_requirements: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - ">="
211
+ - !ruby/object:Gem::Version
212
+ version: '0'
199
213
  - !ruby/object:Gem::Dependency
200
214
  name: salsify_rubocop
201
215
  requirement: !ruby/object:Gem::Requirement
@@ -259,13 +273,13 @@ executables: []
259
273
  extensions: []
260
274
  extra_rdoc_files: []
261
275
  files:
276
+ - ".circleci/config.yml"
262
277
  - ".gitignore"
263
278
  - ".overcommit.yml"
264
279
  - ".rspec"
265
280
  - ".rubocop.yml"
266
281
  - ".ruby-gemset"
267
282
  - ".ruby-version"
268
- - ".travis.yml"
269
283
  - Appraisals
270
284
  - CHANGELOG.md
271
285
  - Gemfile
@@ -278,15 +292,19 @@ files:
278
292
  - gemfiles/.bundle/config
279
293
  - gemfiles/avro1_10_rails5_2.gemfile
280
294
  - gemfiles/avro1_10_rails6_0.gemfile
295
+ - gemfiles/avro1_10_rails6_1.gemfile
281
296
  - gemfiles/avro1_8_rails5_2.gemfile
282
297
  - gemfiles/avro1_9_rails5_2.gemfile
283
298
  - gemfiles/avro1_9_rails6_0.gemfile
299
+ - gemfiles/avro1_9_rails6_1.gemfile
284
300
  - gemfiles/avro_patches_rails5_2.gemfile
285
301
  - gemfiles/avro_patches_rails6_0.gemfile
302
+ - gemfiles/avro_patches_rails6_1.gemfile
286
303
  - lib/avromatic.rb
287
304
  - lib/avromatic/io.rb
288
305
  - lib/avromatic/io/datum_reader.rb
289
306
  - lib/avromatic/io/datum_writer.rb
307
+ - lib/avromatic/io/union_datum.rb
290
308
  - lib/avromatic/messaging.rb
291
309
  - lib/avromatic/model.rb
292
310
  - lib/avromatic/model/attributes.rb
@@ -324,8 +342,6 @@ files:
324
342
  - lib/avromatic/model/validation_error.rb
325
343
  - lib/avromatic/model/value_object.rb
326
344
  - lib/avromatic/model_registry.rb
327
- - lib/avromatic/patches.rb
328
- - lib/avromatic/patches/schema_validator_patch.rb
329
345
  - lib/avromatic/railtie.rb
330
346
  - lib/avromatic/rspec.rb
331
347
  - lib/avromatic/version.rb
@@ -1,16 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.5.8
4
- - 2.6.6
5
- - 2.7.1
6
- before_script:
7
- - bundle exec rubocop
8
- gemfile:
9
- - gemfiles/avro1_8_rails5_2.gemfile
10
- - gemfiles/avro1_9_rails5_2.gemfile
11
- - gemfiles/avro1_10_rails5_2.gemfile
12
- - gemfiles/avro1_9_rails6_0.gemfile
13
- - gemfiles/avro1_10_rails6_0.gemfile
14
- - gemfiles/avro_patches_rails5_2.gemfile
15
- - gemfiles/avro_patches_rails6_0.gemfile
16
- script: bundle exec rspec
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- loaded_avro_patches = begin
4
- require 'avro-patches'
5
- true
6
- rescue LoadError
7
- false
8
- end
9
-
10
- if loaded_avro_patches
11
- require 'avromatic/patches/schema_validator_patch'
12
- avro_patches_version = Gem::Version.new(AvroPatches::VERSION)
13
- if avro_patches_version < Gem::Version.new('0.4.0')
14
- Avro::SchemaValidator.singleton_class.prepend(Avromatic::Patches::SchemaValidatorPatch)
15
- else
16
- Avro::SchemaValidator.singleton_class.prepend(Avromatic::Patches::SchemaValidatorPatchV040)
17
- end
18
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Avromatic
4
- module Patches
5
- module SchemaValidatorPatch
6
- # This method replaces validate_recursive in AvroPatches::LogicalTypes::SchemaValidatorPatch
7
- # to enable validating datums that contain an encoding provider.
8
- def validate_recursive(expected_schema, logical_datum, path, result, encoded = false)
9
- datum = resolve_datum(expected_schema, logical_datum, encoded)
10
- case expected_schema.type_sym
11
- when :record, :error, :request
12
- if datum.is_a?(Hash) && datum.key?(Avromatic::IO::ENCODING_PROVIDER)
13
- return if expected_schema.sha256_resolution_fingerprint ==
14
- datum[Avromatic::IO::ENCODING_PROVIDER].value_avro_schema.sha256_resolution_fingerprint
15
- raise Avro::SchemaValidator::ValidationError
16
- end
17
- end
18
- super(expected_schema, logical_datum, path, result, encoded)
19
- end
20
- end
21
-
22
- module SchemaValidatorPatchV040
23
- # This method replaces validate_recursive in AvroPatches::LogicalTypes::SchemaValidatorPatch
24
- # to enable validating datums that contain an encoding provider.
25
- def validate_recursive(expected_schema, logical_datum, path, result, options = {})
26
- datum = resolve_datum(expected_schema, logical_datum, options[:encoded])
27
- case expected_schema.type_sym
28
- when :record, :error, :request
29
- if datum.is_a?(Hash) && datum.key?(Avromatic::IO::ENCODING_PROVIDER)
30
- return if expected_schema.sha256_resolution_fingerprint ==
31
- datum[Avromatic::IO::ENCODING_PROVIDER].value_avro_schema.sha256_resolution_fingerprint
32
- raise Avro::SchemaValidator::ValidationError
33
- end
34
- end
35
- super(expected_schema, logical_datum, path, result, options)
36
- end
37
- end
38
- end
39
- end