avromatic 3.0.0 → 4.1.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +11 -12
  3. data/.github/CODEOWNERS +1 -0
  4. data/.rubocop.yml +2 -2
  5. data/.ruby-version +1 -1
  6. data/Appraisals +15 -15
  7. data/CHANGELOG.md +18 -0
  8. data/README.md +24 -3
  9. data/avromatic.gemspec +15 -6
  10. data/gemfiles/{avro1_9_rails5_2.gemfile → avro1_10_rails7_0.gemfile} +3 -3
  11. data/gemfiles/{avro1_9_rails6_1.gemfile → avro1_11_rails6_1.gemfile} +1 -1
  12. data/gemfiles/{avro1_9_rails6_0.gemfile → avro1_11_rails7_0.gemfile} +3 -3
  13. data/lib/avromatic/io/datum_writer.rb +1 -3
  14. data/lib/avromatic/messaging.rb +2 -4
  15. data/lib/avromatic/model/attributes.rb +30 -13
  16. data/lib/avromatic/model/configurable.rb +3 -1
  17. data/lib/avromatic/model/configuration.rb +9 -1
  18. data/lib/avromatic/model/message_decoder.rb +3 -1
  19. data/lib/avromatic/model/messaging_serialization.rb +9 -6
  20. data/lib/avromatic/model/nested_models.rb +4 -2
  21. data/lib/avromatic/model/raw_serialization.rb +6 -5
  22. data/lib/avromatic/model/types/array_type.rb +1 -0
  23. data/lib/avromatic/model/types/custom_type.rb +6 -1
  24. data/lib/avromatic/model/types/enum_type.rb +1 -0
  25. data/lib/avromatic/model/types/fixed_type.rb +1 -0
  26. data/lib/avromatic/model/types/map_type.rb +1 -0
  27. data/lib/avromatic/model/types/record_type.rb +1 -0
  28. data/lib/avromatic/model/types/type_factory.rb +7 -3
  29. data/lib/avromatic/model/types/union_type.rb +2 -3
  30. data/lib/avromatic/model/validation.rb +1 -3
  31. data/lib/avromatic/model_registry.rb +13 -2
  32. data/lib/avromatic/rspec.rb +1 -0
  33. data/lib/avromatic/version.rb +1 -1
  34. data/lib/avromatic.rb +6 -0
  35. metadata +21 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d90a142c407a453ef9007373f215d8acd137937e54b46aa2c29e8edcf8440aef
4
- data.tar.gz: c8eecbb9860859346d9a67e3e2a78a2e38447bdff4d4fb9208f57bcf89940215
3
+ metadata.gz: edd31aa475804bf20a1f5b4ddbe90e8c98460617daeaf2e1641316c08cce505b
4
+ data.tar.gz: b20ae31b2aeff034f9fa99e2a90d4bb78dfe894967ab8e0267b1df6ee954a15d
5
5
  SHA512:
6
- metadata.gz: 77b078294c887e298e4ede5f1f47cfccaccffd0b065ad1d4bad9820d2af8c77e522462282d1d44db35acefaf6fc301ccb7a59dfda844c5ebee297d51792ee912
7
- data.tar.gz: 8f57c2afee1266f790c031920efb7e1690420e4c6de36d22af3e11361bc5bf7eaabb3eba4e2974a994a7f7509e3db01335c954895c981db4069a8c7828b483de
6
+ metadata.gz: 6e4d441e7645b52840725041d361f699266a9e5dd974e8cd588c8f4750ba21eb4efd016b9c6c20e034a98ed79b673f9aa12a5f0b3a56312a57b344659b93606d
7
+ data.tar.gz: 5b8ba9cc0049f80a31f88399ca504796997038b969d728be5867d6fd5bc6868786e4ae9dffc0de8a7c1db99e129f9398e4f867794dbd449d54f74c69650735bb
data/.circleci/config.yml CHANGED
@@ -2,14 +2,14 @@ version: 2.1
2
2
  jobs:
3
3
  lint:
4
4
  docker:
5
- - image: salsify/ruby_ci:2.5.8
5
+ - image: salsify/ruby_ci:2.7.4
6
6
  working_directory: ~/avromatic
7
7
  steps:
8
8
  - checkout
9
9
  - restore_cache:
10
10
  keys:
11
- - v2-gems-ruby-2.5.8-{{ checksum "avromatic.gemspec" }}-{{ checksum "Gemfile" }}
12
- - v2-gems-ruby-2.5.8-
11
+ - v2-gems-ruby-2.7.4-{{ checksum "avromatic.gemspec" }}-{{ checksum "Gemfile" }}
12
+ - v2-gems-ruby-2.7.4-
13
13
  - run:
14
14
  name: Install Gems
15
15
  command: |
@@ -18,7 +18,7 @@ jobs:
18
18
  bundle clean
19
19
  fi
20
20
  - save_cache:
21
- key: v2-gems-ruby-2.5.8-{{ checksum "avromatic.gemspec" }}-{{ checksum "Gemfile" }}
21
+ key: v2-gems-ruby-2.7.4-{{ checksum "avromatic.gemspec" }}-{{ checksum "Gemfile" }}
22
22
  paths:
23
23
  - "vendor/bundle"
24
24
  - "gemfiles/vendor/bundle"
@@ -69,21 +69,20 @@ workflows:
69
69
  matrix:
70
70
  parameters:
71
71
  gemfile:
72
- - "gemfiles/avro1_9_rails5_2.gemfile"
73
72
  - "gemfiles/avro1_10_rails5_2.gemfile"
74
- - "gemfiles/avro1_9_rails6_0.gemfile"
75
73
  - "gemfiles/avro1_10_rails6_0.gemfile"
76
74
  - "gemfiles/avro1_10_rails6_1.gemfile"
77
- - "gemfiles/avro1_9_rails6_1.gemfile"
75
+ - "gemfiles/avro1_10_rails7_0.gemfile"
76
+ - "gemfiles/avro1_11_rails7_0.gemfile"
78
77
  ruby-version:
79
- - "2.5.8"
80
- - "2.6.6"
81
- - "2.7.2"
78
+ - "2.7.4"
82
79
  - test:
83
80
  matrix:
84
81
  parameters:
85
82
  gemfile:
86
83
  - "gemfiles/avro1_10_rails6_1.gemfile"
87
- - "gemfiles/avro1_9_rails6_1.gemfile"
84
+ - "gemfiles/avro1_10_rails7_0.gemfile"
85
+ - "gemfiles/avro1_11_rails7_0.gemfile"
88
86
  ruby-version:
89
- - "3.0.0"
87
+ - "3.0.3"
88
+ - "3.1.0"
@@ -0,0 +1 @@
1
+ * @jturkel @kphelps @tjwp
data/.rubocop.yml CHANGED
@@ -2,10 +2,10 @@ inherit_gem:
2
2
  salsify_rubocop: conf/rubocop.yml
3
3
 
4
4
  AllCops:
5
- TargetRubyVersion: 2.5
5
+ TargetRubyVersion: 2.7
6
6
  Exclude:
7
7
  - 'vendor/**/*'
8
- - 'gemfiles/vendor/**/*'
8
+ - 'gemfiles/**/*'
9
9
 
10
10
  Style/MultilineBlockChain:
11
11
  Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.5.8
1
+ 2.7.4
data/Appraisals CHANGED
@@ -1,37 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- appraise 'avro1_9-rails5_2' do
4
- gem 'avro', '1.9.2'
5
- gem 'activesupport', '~> 5.2.0'
6
- gem 'activemodel', '~> 5.2.0'
7
- end
8
-
9
3
  appraise 'avro1_10-rails5_2' do
10
4
  gem 'avro', '~> 1.10.0'
11
5
  gem 'activesupport', '~> 5.2.0'
12
6
  gem 'activemodel', '~> 5.2.0'
13
7
  end
14
8
 
15
- appraise 'avro1_9-rails6_0' do
16
- gem 'avro', '1.9.2'
17
- gem 'activesupport', '~> 6.0.0'
18
- gem 'activemodel', '~> 6.0.0'
19
- end
20
-
21
9
  appraise 'avro1_10-rails6_0' do
22
10
  gem 'avro', '~> 1.10.0'
23
11
  gem 'activesupport', '~> 6.0.0'
24
12
  gem 'activemodel', '~> 6.0.0'
25
13
  end
26
14
 
27
- appraise 'avro1_9-rails6_1' do
28
- gem 'avro', '1.9.2'
15
+ appraise 'avro1_10-rails6_1' do
16
+ gem 'avro', '~> 1.10.0'
29
17
  gem 'activesupport', '~> 6.1.0'
30
18
  gem 'activemodel', '~> 6.1.0'
31
19
  end
32
20
 
33
- appraise 'avro1_10-rails6_1' do
21
+ appraise 'avro1_10-rails7_0' do
34
22
  gem 'avro', '~> 1.10.0'
23
+ gem 'activesupport', '~> 7.0.0'
24
+ gem 'activemodel', '~> 7.0.0'
25
+ end
26
+
27
+ appraise 'avro1_11-rails6_1' do
28
+ gem 'avro', '~> 1.11.0'
35
29
  gem 'activesupport', '~> 6.1.0'
36
30
  gem 'activemodel', '~> 6.1.0'
37
31
  end
32
+
33
+ appraise 'avro1_11-rails7_0' do
34
+ gem 'avro', '~> 1.11.0'
35
+ gem 'activesupport', '~> 7.0.0'
36
+ gem 'activemodel', '~> 7.0.0'
37
+ end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # avromatic changelog
2
2
 
3
+ ## 4.1.0
4
+ - Add support for specifying a subject for the avro schema when building an Avromatic model
5
+
6
+ ## 4.0.0
7
+ - Drop support for Ruby 2.6.
8
+ - Drop support for Avro 1.9.
9
+ - Add support for Avro 1.11.
10
+ - Add support for Rails 7.0.
11
+
12
+ ## 3.0.2
13
+ - Reset the schema registry client between RSpec tests to ensure any cached values are
14
+ consistent with the fake schema registry.
15
+
16
+ ## 3.0.1
17
+ - Raise an error when registering a nested model that has already been auto-generated.
18
+ This avoids hard to troubleshoot coercion errors when instantiating models and fixes
19
+ a regression introduced in Avromatic 2.2.2.
20
+
3
21
  ## 3.0.0
4
22
  - Drop support for Ruby 2.4.
5
23
  - Add support for Ruby 3.0.
data/README.md CHANGED
@@ -72,7 +72,9 @@ and the [Messaging API](#messaging-api).
72
72
  so that they can be referenced by id. Either `schema_registry` or
73
73
  `registry_url` must be configured.
74
74
  * **registry_url**: URL for the schema registry. Either `schema_registry` or
75
- `registry_url` must be configured.
75
+ `registry_url` must be configured. The `build_schema_registry!` method may
76
+ be used to create a caching schema registry client instance based on other
77
+ configuration values.
76
78
  * **use_schema_fingerprint_lookup**: Avromatic supports a Schema Registry
77
79
  [extension](https://github.com/salsify/avro-schema-registry#extensions) that
78
80
  provides an endpoint to lookup existing schema ids by fingerprint.
@@ -91,6 +93,7 @@ Example using a schema registry:
91
93
  Avromatic.configure do |config|
92
94
  config.schema_store = AvroTurf::SchemaStore.new(path: 'avro/schemas')
93
95
  config.registry_url = Rails.configuration.x.avro_schema_registry_url
96
+ config.build_schema_registry!
94
97
  config.build_messaging!
95
98
  end
96
99
  ```
@@ -121,7 +124,7 @@ The Avro schema can be specified by name and loaded using the schema store:
121
124
 
122
125
  ```ruby
123
126
  class MyModel
124
- include Avromatic::Model.build(schema_name :my_model)
127
+ include Avromatic::Model.build(schema_name: :my_model)
125
128
  end
126
129
 
127
130
  # Construct instances by passing in a hash of attributes
@@ -153,12 +156,20 @@ class MyModel
153
156
  end
154
157
  ```
155
158
 
159
+ A specific subject name can be associated with the schema:
160
+ ```ruby
161
+ class MyModel
162
+ include Avromatic::Model.build(schema_name: 'my_model',
163
+ schema_subject: 'my_model-value')
164
+ end
165
+ ```
166
+
156
167
  Models are generated as immutable value
157
168
  objects by default, but can optionally be defined as mutable:
158
169
 
159
170
  ```ruby
160
171
  class MyModel
161
- include Avromatic::Model.build(schema_name :my_model, mutable: true)
172
+ include Avromatic::Model.build(schema_name: :my_model, mutable: true)
162
173
  end
163
174
  ```
164
175
 
@@ -192,6 +203,16 @@ class MyTopic
192
203
  end
193
204
  ```
194
205
 
206
+ A specific subject name can be associated with both the value and key schemas:
207
+ ```ruby
208
+ class MyTopic
209
+ include Avromatic::Model.build(value_schema_name: :topic_value,
210
+ value_schema_subject: 'topic_value-value',
211
+ key_schema_name: :topic_key,
212
+ key_schema_subject: 'topic_key-value')
213
+ end
214
+ ```
215
+
195
216
  A model can also be generated as an anonymous class that can be assigned to a
196
217
  constant:
197
218
 
data/avromatic.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'avromatic/version'
6
6
 
@@ -15,16 +15,25 @@ Gem::Specification.new do |spec|
15
15
  spec.homepage = 'https://github.com/salsify/avromatic.git'
16
16
  spec.license = 'MIT'
17
17
 
18
+ if spec.respond_to?(:metadata)
19
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
20
+ spec.metadata['rubygems_mfa_required'] = 'true'
21
+ else
22
+ raise 'RubyGems 2.0 or newer is required to set allowed_push_host.'
23
+ end
24
+
18
25
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
26
  spec.bindir = 'exe'
20
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
28
  spec.require_paths = ['lib']
22
29
 
23
- spec.required_ruby_version = '>= 2.5'
30
+ spec.metadata['rubygems_mfa_required'] = 'true'
31
+
32
+ spec.required_ruby_version = '>= 2.7'
24
33
 
25
- spec.add_runtime_dependency 'activemodel', '>= 5.2', '< 6.2'
26
- spec.add_runtime_dependency 'activesupport', '>= 5.2', '< 6.2'
27
- spec.add_runtime_dependency 'avro', '>= 1.9.0', '< 1.11'
34
+ spec.add_runtime_dependency 'activemodel', '>= 5.2', '< 7.1'
35
+ spec.add_runtime_dependency 'activesupport', '>= 5.2', '< 7.1'
36
+ spec.add_runtime_dependency 'avro', '>= 1.10.0', '< 1.12'
28
37
  spec.add_runtime_dependency 'avro_schema_registry-client', '>= 0.4.0'
29
38
  spec.add_runtime_dependency 'avro_turf'
30
39
  spec.add_runtime_dependency 'ice_nine'
@@ -36,7 +45,7 @@ Gem::Specification.new do |spec|
36
45
  spec.add_development_dependency 'rake', '~> 13.0'
37
46
  spec.add_development_dependency 'rspec', '~> 3.8'
38
47
  spec.add_development_dependency 'rspec_junit_formatter'
39
- spec.add_development_dependency 'salsify_rubocop', '~> 0.52.1.1'
48
+ spec.add_development_dependency 'salsify_rubocop', '~> 1.1.0'
40
49
  spec.add_development_dependency 'simplecov'
41
50
  spec.add_development_dependency 'webmock'
42
51
  # For AvroSchemaRegistry::FakeServer
@@ -2,8 +2,8 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "avro", "1.9.2"
6
- gem "activesupport", "~> 5.2.0"
7
- gem "activemodel", "~> 5.2.0"
5
+ gem "avro", "~> 1.10.0"
6
+ gem "activesupport", "~> 7.0.0"
7
+ gem "activemodel", "~> 7.0.0"
8
8
 
9
9
  gemspec path: "../"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "avro", "1.9.2"
5
+ gem "avro", "~> 1.11.0"
6
6
  gem "activesupport", "~> 6.1.0"
7
7
  gem "activemodel", "~> 6.1.0"
8
8
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "avro", "1.9.2"
6
- gem "activesupport", "~> 6.0.0"
7
- gem "activemodel", "~> 6.0.0"
5
+ gem "avro", "~> 1.11.0"
6
+ gem "activesupport", "~> 7.0.0"
7
+ gem "activemodel", "~> 7.0.0"
8
8
 
9
9
  gemspec path: "../"
@@ -21,9 +21,7 @@ module Avromatic
21
21
  end
22
22
  end
23
23
 
24
- unless index_of_schema
25
- raise Avro::IO::AvroTypeError.new(writers_schema, datum)
26
- end
24
+ raise Avro::IO::AvroTypeError.new(writers_schema, datum) unless index_of_schema
27
25
 
28
26
  encoder.write_long(index_of_schema)
29
27
  write_data(writers_schema.schemas[index_of_schema], datum, encoder)
@@ -16,12 +16,10 @@ module Avromatic
16
16
  # The first byte is MAGIC!!!
17
17
  magic_byte = decoder.read(1)
18
18
 
19
- if magic_byte != MAGIC_BYTE
20
- raise "Expected data to begin with a magic byte, got `#{magic_byte.inspect}`"
21
- end
19
+ raise "Expected data to begin with a magic byte, got `#{magic_byte.inspect}`" if magic_byte != MAGIC_BYTE
22
20
 
23
21
  # The schema id is a 4-byte big-endian integer.
24
- schema_id = decoder.read(4).unpack('N').first
22
+ schema_id = decoder.read(4).unpack1('N')
25
23
 
26
24
  writers_schema = @schemas_by_id.fetch(schema_id) do
27
25
  schema_json = @registry.fetch(schema_id)
@@ -22,6 +22,7 @@ module Avromatic
22
22
 
23
23
  class AttributeDefinition
24
24
  attr_reader :name, :name_string, :setter_name, :type, :field, :default, :owner
25
+
25
26
  delegate :serialize, to: :type
26
27
 
27
28
  def initialize(owner:, field:, type:)
@@ -58,21 +59,31 @@ module Avromatic
58
59
  def coerce(input)
59
60
  type.coerce(input)
60
61
  rescue Avromatic::Model::UnknownAttributeError => e
61
- raise Avromatic::Model::CoercionError.new("Value for #{owner.name}##{name} could not be coerced to a #{type.name} " \
62
+ raise Avromatic::Model::CoercionError.new(
63
+ "Value for #{owner.name}##{name} could not be coerced to a #{type.name} " \
62
64
  "because the following unexpected attributes were provided: #{e.unknown_attributes.join(', ')}. " \
63
65
  "Only the following attributes are allowed: #{e.allowed_attributes.join(', ')}. " \
64
- "Provided argument: #{input.inspect}")
66
+ "Provided argument: #{input.inspect}"
67
+ )
65
68
  rescue StandardError
66
69
  if type.input_classes && type.input_classes.none? { |input_class| input.is_a?(input_class) }
67
- raise Avromatic::Model::CoercionError.new("Value for #{owner.name}##{name} could not be coerced to a #{type.name} " \
68
- "because a #{input.class.name} was provided but expected a #{type.input_classes.map(&:name).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')}. " \
69
- "Provided argument: #{input.inspect}")
70
+ raise Avromatic::Model::CoercionError.new(
71
+ "Value for #{owner.name}##{name} could not be coerced to a #{type.name} " \
72
+ "because a #{input.class.name} was provided but expected a #{type.input_classes.map(&:name).to_sentence(
73
+ two_words_connector: ' or ', last_word_connector: ', or '
74
+ )}. " \
75
+ "Provided argument: #{input.inspect}"
76
+ )
70
77
  elsif input.is_a?(Hash) && type.is_a?(Avromatic::Model::Types::UnionType)
71
- raise Avromatic::Model::CoercionError.new("Value for #{owner.name}##{name} could not be coerced to a #{type.name} " \
72
- "because no union member type matches the provided attributes: #{input.inspect}")
78
+ raise Avromatic::Model::CoercionError.new(
79
+ "Value for #{owner.name}##{name} could not be coerced to a #{type.name} " \
80
+ "because no union member type matches the provided attributes: #{input.inspect}"
81
+ )
73
82
  else
74
- raise Avromatic::Model::CoercionError.new("Value for #{owner.name}##{name} could not be coerced to a #{type.name}. " \
75
- "Provided argument: #{input.inspect}")
83
+ raise Avromatic::Model::CoercionError.new(
84
+ "Value for #{owner.name}##{name} could not be coerced to a #{type.name}. " \
85
+ "Provided argument: #{input.inspect}"
86
+ )
76
87
  end
77
88
  end
78
89
  end
@@ -106,7 +117,8 @@ module Avromatic
106
117
  unknown_attributes = (data.keys.map(&:to_s) - _attributes.keys.map(&:to_s)).sort
107
118
  allowed_attributes = attribute_definitions.keys.map(&:to_s).sort
108
119
  message = "Unexpected arguments for #{self.class.name}#initialize: #{unknown_attributes.join(', ')}. " \
109
- "Only the following arguments are allowed: #{allowed_attributes.join(', ')}. Provided arguments: #{data.inspect}"
120
+ "Only the following arguments are allowed: #{allowed_attributes.join(', ')}. " \
121
+ "Provided arguments: #{data.inspect}"
110
122
  raise Avromatic::Model::UnknownAttributeError.new(message, unknown_attributes: unknown_attributes,
111
123
  allowed_attributes: allowed_attributes)
112
124
  end
@@ -136,8 +148,8 @@ module Avromatic
136
148
  begin
137
149
  define_avro_attributes(key_avro_schema, generated_methods_module,
138
150
  allow_optional: config.allow_optional_key_fields)
139
- rescue OptionalFieldError => ex
140
- raise "Optional field '#{ex.field.name}' not allowed in key schema."
151
+ rescue OptionalFieldError => e
152
+ raise "Optional field '#{e.field.name}' not allowed in key schema."
141
153
  end
142
154
  end
143
155
  define_avro_attributes(avro_schema, generated_methods_module)
@@ -155,6 +167,7 @@ module Avromatic
155
167
  conflicts =
156
168
  (key_avro_field_names & value_avro_field_names).each_with_object([]) do |name, msgs|
157
169
  next unless schema_fields_differ?(name)
170
+
158
171
  msgs << "Field '#{name}' has a different type in each schema: "\
159
172
  "value #{value_avro_fields_by_name[name]}, "\
160
173
  "key #{key_avro_fields_by_name[name]}"
@@ -190,7 +203,11 @@ module Avromatic
190
203
 
191
204
  # Add all generated methods to a module so they can be overridden
192
205
  generated_methods_module.send(:define_method, field.name) { _attributes[symbolized_field_name] }
193
- generated_methods_module.send(:define_method, "#{field.name}?") { !!_attributes[symbolized_field_name] } if FieldHelper.boolean?(field)
206
+ if FieldHelper.boolean?(field)
207
+ generated_methods_module.send(:define_method, "#{field.name}?") do
208
+ !!_attributes[symbolized_field_name]
209
+ end
210
+ end
194
211
 
195
212
  generated_methods_module.send(:define_method, "#{field.name}=") do |value|
196
213
  _attributes[symbolized_field_name] = attribute_definition.coerce(value)
@@ -24,7 +24,8 @@ module Avromatic
24
24
  end
25
25
 
26
26
  module ClassMethods
27
- delegate :avro_schema, :value_avro_schema, :key_avro_schema, :mutable?, :immutable?, to: :config
27
+ delegate :avro_schema, :value_avro_schema, :key_avro_schema, :mutable?, :immutable?,
28
+ :avro_schema_subject, :value_avro_schema_subject, :key_avro_schema_subject, to: :config
28
29
 
29
30
  def value_avro_field_names
30
31
  @value_avro_field_names ||= value_avro_schema.fields.map(&:name).map(&:to_sym).freeze
@@ -68,6 +69,7 @@ module Avromatic
68
69
  end
69
70
 
70
71
  delegate :avro_schema, :value_avro_schema, :key_avro_schema,
72
+ :avro_schema_subject, :value_avro_schema_subject, :key_avro_schema_subject,
71
73
  :value_avro_field_names, :key_avro_field_names,
72
74
  :value_avro_field_references, :key_avro_field_references,
73
75
  :mutable?, :immutable?,
@@ -7,7 +7,7 @@ module Avromatic
7
7
  class Configuration
8
8
 
9
9
  attr_reader :avro_schema, :key_avro_schema, :nested_models, :mutable,
10
- :allow_optional_key_fields
10
+ :allow_optional_key_fields, :avro_schema_subject, :key_avro_schema_subject
11
11
  alias_method :mutable?, :mutable
12
12
  delegate :schema_store, to: Avromatic
13
13
 
@@ -17,23 +17,30 @@ module Avromatic
17
17
  # @param options [Hash]
18
18
  # @option options [Avro::Schema] :schema
19
19
  # @option options [String, Symbol] :schema_name
20
+ # @option options [String, Symbol] :schema_subject
20
21
  # @option options [Avro::Schema] :value_schema
21
22
  # @option options [String, Symbol] :value_schema_name
23
+ # @option options [String, Symbol] :value_schema_subject
22
24
  # @option options [Avro::Schema] :key_schema
23
25
  # @option options [String, Symbol] :key_schema_name
26
+ # @option options [String, Symbol] :key_schema_subject
24
27
  # @option options [Avromatic::ModelRegistry] :nested_models
25
28
  # @option options [Boolean] :mutable, default false
26
29
  # @option options [Boolean] :allow_optional_key_fields, default false
27
30
  def initialize(**options)
28
31
  @avro_schema = find_avro_schema(**options)
32
+ @avro_schema_subject = options[:schema_subject] || options[:value_schema_subject]
29
33
  raise ArgumentError.new('value_schema(_name) or schema(_name) must be specified') unless avro_schema
34
+
30
35
  @key_avro_schema = find_schema_by_option(:key_schema, **options)
36
+ @key_avro_schema_subject = options[:key_schema_subject]
31
37
  @nested_models = options[:nested_models]
32
38
  @mutable = options.fetch(:mutable, false)
33
39
  @allow_optional_key_fields = options.fetch(:allow_optional_key_fields, false)
34
40
  end
35
41
 
36
42
  alias_method :value_avro_schema, :avro_schema
43
+ alias_method :value_avro_schema_subject, :avro_schema_subject
37
44
 
38
45
  def immutable?
39
46
  !mutable?
@@ -46,6 +53,7 @@ module Avromatic
46
53
  (options[:schema] || options[:schema_name])
47
54
  raise ArgumentError.new('Only one of value_schema(_name) and schema(_name) can be specified')
48
55
  end
56
+
49
57
  find_schema_by_option(:value_schema, **options) || find_schema_by_option(:schema, **options)
50
58
  end
51
59
 
@@ -89,6 +89,7 @@ module Avromatic
89
89
  message_key, message_value = extract_key_and_value(*args)
90
90
  model_key = model_key_for_message(message_key, message_value)
91
91
  raise UnexpectedKeyError.new(*model_key) unless model_map.key?(model_key)
92
+
92
93
  [model_map[model_key], message_key, message_value]
93
94
  end
94
95
 
@@ -106,7 +107,7 @@ module Avromatic
106
107
  end
107
108
 
108
109
  def extract_schema_id(data)
109
- data[1..4].unpack('N').first
110
+ data[1..4].unpack1('N')
110
111
  end
111
112
 
112
113
  def validate_magic_byte!(data)
@@ -118,6 +119,7 @@ module Avromatic
118
119
  models.each_with_object(Hash.new) do |model, map|
119
120
  key = model_key(model)
120
121
  raise DuplicateKeyError.new(map[key], model) if map.key?(key) && !model.equal?(map[key])
122
+
121
123
  map[key] = model
122
124
  end
123
125
  end
@@ -16,15 +16,18 @@ module Avromatic
16
16
  def avro_message_value
17
17
  avro_messaging.encode(
18
18
  value_attributes_for_avro,
19
- schema_name: value_avro_schema.fullname
19
+ schema_name: value_avro_schema.fullname,
20
+ subject: value_avro_schema_subject
20
21
  )
21
22
  end
22
23
 
23
24
  def avro_message_key
24
25
  raise 'Model has no key schema' unless key_avro_schema
26
+
25
27
  avro_messaging.encode(
26
28
  key_attributes_for_avro,
27
- schema_name: key_avro_schema.fullname
29
+ schema_name: key_avro_schema.fullname,
30
+ subject: key_avro_schema_subject
28
31
  )
29
32
  end
30
33
  end
@@ -55,15 +58,15 @@ module Avromatic
55
58
 
56
59
  module Registration
57
60
  def register_schemas!
58
- register_schema(key_avro_schema) if key_avro_schema
59
- register_schema(value_avro_schema)
61
+ register_schema(key_avro_schema, subject: key_avro_schema_subject) if key_avro_schema
62
+ register_schema(value_avro_schema, subject: value_avro_schema_subject)
60
63
  nil
61
64
  end
62
65
 
63
66
  private
64
67
 
65
- def register_schema(schema)
66
- avro_messaging.registry.register(schema.fullname, schema)
68
+ def register_schema(schema, subject: nil)
69
+ avro_messaging.registry.register(subject || schema.fullname, schema)
67
70
  end
68
71
  end
69
72
 
@@ -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
@@ -36,6 +36,7 @@ module Avromatic
36
36
  end
37
37
 
38
38
  raise 'Model has no key schema' unless key_avro_schema
39
+
39
40
  avro_raw_encode(key_attributes_for_avro, :key)
40
41
  end
41
42
 
@@ -118,7 +119,7 @@ module Avromatic
118
119
  # If supplied then the key_schema and value_schema are used as the writer's
119
120
  # schema for the corresponding value. The model's schemas are always used
120
121
  # as the reader's schemas.
121
- def avro_raw_decode(key: nil, value:, key_schema: nil, value_schema: nil)
122
+ def avro_raw_decode(value:, key: nil, key_schema: nil, value_schema: nil)
122
123
  key_attributes = key && decode_avro_datum(key, key_schema, :key)
123
124
  value_attributes = decode_avro_datum(value, value_schema, :value)
124
125
  value_attributes.merge!(key_attributes) if key_attributes
@@ -150,10 +151,10 @@ module Avromatic
150
151
 
151
152
  def datum_writer
152
153
  @datum_writer ||= begin
153
- hash = { value: datum_writer_class.new(value_avro_schema) }
154
- hash[:key] = datum_writer_class.new(key_avro_schema) if key_avro_schema
155
- hash
156
- end
154
+ hash = { value: datum_writer_class.new(value_avro_schema) }
155
+ hash[:key] = datum_writer_class.new(key_avro_schema) if key_avro_schema
156
+ hash
157
+ end
157
158
  end
158
159
 
159
160
  def datum_reader
@@ -11,6 +11,7 @@ module Avromatic
11
11
  attr_reader :value_type
12
12
 
13
13
  def initialize(value_type:)
14
+ super()
14
15
  @value_type = value_type
15
16
  end
16
17
 
@@ -11,6 +11,7 @@ module Avromatic
11
11
  attr_reader :custom_type_configuration, :value_classes, :default_type
12
12
 
13
13
  def initialize(custom_type_configuration:, default_type:)
14
+ super()
14
15
  @custom_type_configuration = custom_type_configuration
15
16
  @default_type = default_type
16
17
  @deserializer = custom_type_configuration.deserializer || IDENTITY_PROC
@@ -27,7 +28,11 @@ module Avromatic
27
28
  end
28
29
 
29
30
  def name
30
- custom_type_configuration.value_class ? custom_type_configuration.value_class.name.to_s.freeze : default_type.name
31
+ if custom_type_configuration.value_class
32
+ custom_type_configuration.value_class.name.to_s.freeze
33
+ else
34
+ default_type.name
35
+ end
31
36
  end
32
37
 
33
38
  def coerce(input)
@@ -12,6 +12,7 @@ module Avromatic
12
12
  attr_reader :allowed_values
13
13
 
14
14
  def initialize(allowed_values)
15
+ super()
15
16
  @allowed_values = allowed_values.to_set
16
17
  end
17
18
 
@@ -11,6 +11,7 @@ module Avromatic
11
11
  attr_reader :size
12
12
 
13
13
  def initialize(size)
14
+ super()
14
15
  @size = size
15
16
  end
16
17
 
@@ -11,6 +11,7 @@ module Avromatic
11
11
  attr_reader :value_type, :key_type
12
12
 
13
13
  def initialize(key_type:, value_type:)
14
+ super()
14
15
  @key_type = key_type
15
16
  @value_type = value_type
16
17
  end
@@ -9,6 +9,7 @@ module Avromatic
9
9
  attr_reader :record_class, :value_classes, :input_classes
10
10
 
11
11
  def initialize(record_class:)
12
+ super()
12
13
  @record_class = record_class
13
14
  @value_classes = [record_class].freeze
14
15
  @input_classes = [record_class, Hash].freeze
@@ -59,10 +59,12 @@ module Avromatic
59
59
  when :enum
60
60
  Avromatic::Model::Types::EnumType.new(schema.symbols)
61
61
  when :array
62
- value_type = create(schema: schema.items, nested_models: nested_models, use_custom_types: use_custom_types)
62
+ value_type = create(schema: schema.items, nested_models: nested_models,
63
+ use_custom_types: use_custom_types)
63
64
  Avromatic::Model::Types::ArrayType.new(value_type: value_type)
64
65
  when :map
65
- value_type = create(schema: schema.values, nested_models: nested_models, use_custom_types: use_custom_types)
66
+ value_type = create(schema: schema.values, nested_models: nested_models,
67
+ use_custom_types: use_custom_types)
66
68
  Avromatic::Model::Types::MapType.new(
67
69
  key_type: Avromatic::Model::Types::StringType.new,
68
70
  value_type: value_type
@@ -97,8 +99,10 @@ module Avromatic
97
99
  if nested_models.registered?(fullname)
98
100
  nested_model = nested_models[fullname]
99
101
  unless schema_fingerprint(schema) == schema_fingerprint(nested_model.avro_schema)
100
- raise "The #{nested_model.name} model is already registered with an incompatible version of the #{schema.fullname} schema"
102
+ raise "The #{nested_model.name} model is already registered with an incompatible version of the " \
103
+ "#{schema.fullname} schema"
101
104
  end
105
+
102
106
  nested_model
103
107
  else
104
108
  Avromatic::Model.model(schema: schema, nested_models: nested_models)
@@ -10,6 +10,7 @@ module Avromatic
10
10
  attr_reader :member_types, :value_classes, :input_classes
11
11
 
12
12
  def initialize(member_types:)
13
+ super()
13
14
  @member_types = member_types
14
15
  @value_classes = member_types.flat_map(&:value_classes)
15
16
  @input_classes = member_types.flat_map(&:input_classes).uniq
@@ -31,9 +32,7 @@ module Avromatic
31
32
  end
32
33
  end
33
34
 
34
- if result.nil?
35
- raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}")
36
- end
35
+ raise ArgumentError.new("Could not coerce '#{input.inspect}' to #{name}") if result.nil?
37
36
 
38
37
  result
39
38
  end
@@ -64,9 +64,7 @@ module Avromatic
64
64
  end
65
65
  end
66
66
 
67
- if recursively_immutable?
68
- @missing_attributes = missing_attributes.freeze
69
- end
67
+ @missing_attributes = missing_attributes.freeze if recursively_immutable?
70
68
 
71
69
  missing_attributes
72
70
  end
@@ -24,8 +24,10 @@ module Avromatic
24
24
 
25
25
  def register(model)
26
26
  raise 'models with a key schema are not supported' if model.key_avro_schema
27
+
27
28
  name = model_fullname(model)
28
29
  raise "'#{name}' has already been registered" if registered?(name)
30
+
29
31
  @hash[name] = model
30
32
  end
31
33
 
@@ -42,8 +44,11 @@ module Avromatic
42
44
  def ensure_registered_model(model)
43
45
  name = model_fullname(model)
44
46
  if registered?(name)
45
- unless fetch(name).equal?(model)
46
- raise "attempted to replace existing model #{fetch(name)} with new model #{model} as '#{name}'"
47
+ existing_model = fetch(name)
48
+ unless existing_model.equal?(model)
49
+ raise "Attempted to replace existing Avromatic model #{model_debug_name(existing_model)} with new model " \
50
+ "#{model_debug_name(model)} as '#{name}'. Perhaps '#{model_debug_name(model)}' needs to be eager loaded " \
51
+ 'via the Avromatic eager_load_models setting?'
47
52
  end
48
53
  else
49
54
  register(model)
@@ -65,5 +70,11 @@ module Avromatic
65
70
 
66
71
  value.start_with?('.') ? value.from(1) : value
67
72
  end
73
+
74
+ private
75
+
76
+ def model_debug_name(model)
77
+ model.name || model.to_s
78
+ end
68
79
  end
69
80
  end
@@ -12,6 +12,7 @@ RSpec.configure do |config|
12
12
 
13
13
  WebMock.stub_request(:any, /^#{registry_uri}/).to_rack(AvroSchemaRegistry::FakeServer)
14
14
  AvroSchemaRegistry::FakeServer.clear
15
+ Avromatic.build_schema_registry!
15
16
  Avromatic.build_messaging!
16
17
  end
17
18
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Avromatic
4
- VERSION = '3.0.0'
4
+ VERSION = '4.1.0'
5
5
  end
data/lib/avromatic.rb CHANGED
@@ -34,6 +34,7 @@ module Avromatic
34
34
 
35
35
  def self.build_schema_registry
36
36
  raise 'Avromatic must be configured with a registry_url' unless registry_url
37
+
37
38
  if use_schema_fingerprint_lookup
38
39
  AvroSchemaRegistry::CachedClient.new(
39
40
  AvroSchemaRegistry::Client.new(registry_url, logger: logger)
@@ -45,8 +46,13 @@ module Avromatic
45
46
  end
46
47
  end
47
48
 
49
+ def self.build_schema_registry!
50
+ self.schema_registry = build_schema_registry
51
+ end
52
+
48
53
  def self.build_messaging
49
54
  raise 'Avromatic must be configured with a schema_store' unless schema_store
55
+
50
56
  Avromatic::Messaging.new(
51
57
  registry: schema_registry || build_schema_registry,
52
58
  schema_store: schema_store,
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: 3.0.0
4
+ version: 4.1.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: 2021-04-22 00:00:00.000000000 Z
11
+ date: 2022-02-07 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.2'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6.2'
22
+ version: '7.1'
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.2'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6.2'
32
+ version: '7.1'
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.2'
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '6.2'
42
+ version: '7.1'
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
@@ -49,27 +49,27 @@ dependencies:
49
49
  version: '5.2'
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '6.2'
52
+ version: '7.1'
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: avro
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - ">="
58
58
  - !ruby/object:Gem::Version
59
- version: 1.9.0
59
+ version: 1.10.0
60
60
  - - "<"
61
61
  - !ruby/object:Gem::Version
62
- version: '1.11'
62
+ version: '1.12'
63
63
  type: :runtime
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - ">="
68
68
  - !ruby/object:Gem::Version
69
- version: 1.9.0
69
+ version: 1.10.0
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
- version: '1.11'
72
+ version: '1.12'
73
73
  - !ruby/object:Gem::Dependency
74
74
  name: avro_schema_registry-client
75
75
  requirement: !ruby/object:Gem::Requirement
@@ -216,14 +216,14 @@ dependencies:
216
216
  requirements:
217
217
  - - "~>"
218
218
  - !ruby/object:Gem::Version
219
- version: 0.52.1.1
219
+ version: 1.1.0
220
220
  type: :development
221
221
  prerelease: false
222
222
  version_requirements: !ruby/object:Gem::Requirement
223
223
  requirements:
224
224
  - - "~>"
225
225
  - !ruby/object:Gem::Version
226
- version: 0.52.1.1
226
+ version: 1.1.0
227
227
  - !ruby/object:Gem::Dependency
228
228
  name: simplecov
229
229
  requirement: !ruby/object:Gem::Requirement
@@ -274,6 +274,7 @@ extensions: []
274
274
  extra_rdoc_files: []
275
275
  files:
276
276
  - ".circleci/config.yml"
277
+ - ".github/CODEOWNERS"
277
278
  - ".gitignore"
278
279
  - ".overcommit.yml"
279
280
  - ".rspec"
@@ -293,9 +294,9 @@ files:
293
294
  - gemfiles/avro1_10_rails5_2.gemfile
294
295
  - gemfiles/avro1_10_rails6_0.gemfile
295
296
  - gemfiles/avro1_10_rails6_1.gemfile
296
- - gemfiles/avro1_9_rails5_2.gemfile
297
- - gemfiles/avro1_9_rails6_0.gemfile
298
- - gemfiles/avro1_9_rails6_1.gemfile
297
+ - gemfiles/avro1_10_rails7_0.gemfile
298
+ - gemfiles/avro1_11_rails6_1.gemfile
299
+ - gemfiles/avro1_11_rails7_0.gemfile
299
300
  - lib/avromatic.rb
300
301
  - lib/avromatic/io.rb
301
302
  - lib/avromatic/io/datum_reader.rb
@@ -345,7 +346,9 @@ files:
345
346
  homepage: https://github.com/salsify/avromatic.git
346
347
  licenses:
347
348
  - MIT
348
- metadata: {}
349
+ metadata:
350
+ allowed_push_host: https://rubygems.org
351
+ rubygems_mfa_required: 'true'
349
352
  post_install_message:
350
353
  rdoc_options: []
351
354
  require_paths:
@@ -354,14 +357,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
354
357
  requirements:
355
358
  - - ">="
356
359
  - !ruby/object:Gem::Version
357
- version: '2.5'
360
+ version: '2.7'
358
361
  required_rubygems_version: !ruby/object:Gem::Requirement
359
362
  requirements:
360
363
  - - ">="
361
364
  - !ruby/object:Gem::Version
362
365
  version: '0'
363
366
  requirements: []
364
- rubygems_version: 3.0.3.1
367
+ rubygems_version: 3.2.22
365
368
  signing_key:
366
369
  specification_version: 4
367
370
  summary: Generate Ruby models from Avro schemas