diff --git a/CHANGELOG.md b/CHANGELOG.md index 304cc8f..1b53d1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # avromatic changelog +## 4.1.0 +- Add support for specifying a subject for the avro schema when building an Avromatic model + ## 4.0.0 - Drop support for Ruby 2.6. - Drop support for Avro 1.9. diff --git a/README.md b/README.md index d7e8e56..b0d0fab 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ The Avro schema can be specified by name and loaded using the schema store: ```ruby class MyModel - include Avromatic::Model.build(schema_name :my_model) + include Avromatic::Model.build(schema_name: :my_model) end # Construct instances by passing in a hash of attributes @@ -156,12 +156,20 @@ class MyModel end ``` +A specific subject name can be associated with the schema: +```ruby +class MyModel + include Avromatic::Model.build(schema_name: 'my_model', + schema_subject: 'my_model-value') +end +``` + Models are generated as immutable value objects by default, but can optionally be defined as mutable: ```ruby class MyModel - include Avromatic::Model.build(schema_name :my_model, mutable: true) + include Avromatic::Model.build(schema_name: :my_model, mutable: true) end ``` @@ -195,6 +203,16 @@ class MyTopic end ``` +A specific subject name can be associated with both the value and key schemas: +```ruby +class MyTopic + include Avromatic::Model.build(value_schema_name: :topic_value, + value_schema_subject: 'topic_value-value', + key_schema_name: :topic_key, + key_schema_subject: 'topic_key-value') +end +``` + A model can also be generated as an anonymous class that can be assigned to a constant: diff --git a/lib/avromatic/model/configurable.rb b/lib/avromatic/model/configurable.rb index 05aa64d..c70aec2 100644 --- a/lib/avromatic/model/configurable.rb +++ b/lib/avromatic/model/configurable.rb @@ -24,7 +24,8 @@ def initialize(name) end module ClassMethods - delegate :avro_schema, :value_avro_schema, :key_avro_schema, :mutable?, :immutable?, to: :config + delegate :avro_schema, :value_avro_schema, :key_avro_schema, :mutable?, :immutable?, + :avro_schema_subject, :value_avro_schema_subject, :key_avro_schema_subject, to: :config def value_avro_field_names @value_avro_field_names ||= value_avro_schema.fields.map(&:name).map(&:to_sym).freeze @@ -68,6 +69,7 @@ def mapped_by_name(schema) end delegate :avro_schema, :value_avro_schema, :key_avro_schema, + :avro_schema_subject, :value_avro_schema_subject, :key_avro_schema_subject, :value_avro_field_names, :key_avro_field_names, :value_avro_field_references, :key_avro_field_references, :mutable?, :immutable?, diff --git a/lib/avromatic/model/configuration.rb b/lib/avromatic/model/configuration.rb index ef6d904..507c5a6 100644 --- a/lib/avromatic/model/configuration.rb +++ b/lib/avromatic/model/configuration.rb @@ -7,7 +7,7 @@ module Model class Configuration attr_reader :avro_schema, :key_avro_schema, :nested_models, :mutable, - :allow_optional_key_fields + :allow_optional_key_fields, :avro_schema_subject, :key_avro_schema_subject alias_method :mutable?, :mutable delegate :schema_store, to: Avromatic @@ -17,24 +17,30 @@ class Configuration # @param options [Hash] # @option options [Avro::Schema] :schema # @option options [String, Symbol] :schema_name + # @option options [String, Symbol] :schema_subject # @option options [Avro::Schema] :value_schema # @option options [String, Symbol] :value_schema_name + # @option options [String, Symbol] :value_schema_subject # @option options [Avro::Schema] :key_schema # @option options [String, Symbol] :key_schema_name + # @option options [String, Symbol] :key_schema_subject # @option options [Avromatic::ModelRegistry] :nested_models # @option options [Boolean] :mutable, default false # @option options [Boolean] :allow_optional_key_fields, default false def initialize(**options) @avro_schema = find_avro_schema(**options) + @avro_schema_subject = options[:schema_subject] || options[:value_schema_subject] raise ArgumentError.new('value_schema(_name) or schema(_name) must be specified') unless avro_schema @key_avro_schema = find_schema_by_option(:key_schema, **options) + @key_avro_schema_subject = options[:key_schema_subject] @nested_models = options[:nested_models] @mutable = options.fetch(:mutable, false) @allow_optional_key_fields = options.fetch(:allow_optional_key_fields, false) end alias_method :value_avro_schema, :avro_schema + alias_method :value_avro_schema_subject, :avro_schema_subject def immutable? !mutable? diff --git a/lib/avromatic/model/messaging_serialization.rb b/lib/avromatic/model/messaging_serialization.rb index f233c13..d3bfe07 100644 --- a/lib/avromatic/model/messaging_serialization.rb +++ b/lib/avromatic/model/messaging_serialization.rb @@ -16,7 +16,8 @@ module Encode def avro_message_value avro_messaging.encode( value_attributes_for_avro, - schema_name: value_avro_schema.fullname + schema_name: value_avro_schema.fullname, + subject: value_avro_schema_subject ) end @@ -25,7 +26,8 @@ def avro_message_key avro_messaging.encode( key_attributes_for_avro, - schema_name: key_avro_schema.fullname + schema_name: key_avro_schema.fullname, + subject: key_avro_schema_subject ) end end @@ -56,15 +58,15 @@ def avro_message_attributes(*args) module Registration def register_schemas! - register_schema(key_avro_schema) if key_avro_schema - register_schema(value_avro_schema) + register_schema(key_avro_schema, subject: key_avro_schema_subject) if key_avro_schema + register_schema(value_avro_schema, subject: value_avro_schema_subject) nil end private - def register_schema(schema) - avro_messaging.registry.register(schema.fullname, schema) + def register_schema(schema, subject: nil) + avro_messaging.registry.register(subject || schema.fullname, schema) end end diff --git a/lib/avromatic/version.rb b/lib/avromatic/version.rb index aaa2245..2da4db9 100644 --- a/lib/avromatic/version.rb +++ b/lib/avromatic/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Avromatic - VERSION = '4.0.0' + VERSION = '4.1.0' end diff --git a/spec/avromatic/model/builder_spec.rb b/spec/avromatic/model/builder_spec.rb index 785c68f..74e14f7 100644 --- a/spec/avromatic/model/builder_spec.rb +++ b/spec/avromatic/model/builder_spec.rb @@ -141,6 +141,21 @@ end it_behaves_like "a generated model" + + context "with a specified schema subject" do + let(:schema_subject) { 'test.primitive_types-subject' } + let(:test_class) do + Avromatic::Model.model(schema_name: schema_name, + schema_subject: schema_subject) + end + let(:instance) { test_class.new } + + it_behaves_like "a generated model" + + it "returns the specified schema_subject" do + expect(instance.avro_schema_subject).to eq(schema_subject) + end + end end context "named fields" do @@ -267,6 +282,32 @@ end end + context "with a specified value and key subjects" do + let(:value_schema_subject) { 'test.value-subject' } + let(:key_schema_subject) { 'test.key-subject' } + let(:test_class) do + Avromatic::Model.model(value_schema_name: schema_name, + value_schema_subject: value_schema_subject, + key_schema_name: key_schema_name, + key_schema_subject: key_schema_subject) + end + + let(:instance) { test_class.new } + + it "defines a model with attributes for the key and value" do + expect(attribute_names) + .to match_array(schema.fields.map(&:name) | key_schema.fields.map(&:name)) + end + + it "returns the specified value_schema_subject" do + expect(instance.value_avro_schema_subject).to eq(value_schema_subject) + end + + it "returns the specified key_schema_subject" do + expect(instance.key_avro_schema_subject).to eq(key_schema_subject) + end + end + context "when the key and value have conflicting fields" do let(:key_schema_name) { 'test.key_conflict' } diff --git a/spec/avromatic/model/messaging_serialization_spec.rb b/spec/avromatic/model/messaging_serialization_spec.rb index 1df74e3..c843094 100644 --- a/spec/avromatic/model/messaging_serialization_spec.rb +++ b/spec/avromatic/model/messaging_serialization_spec.rb @@ -8,6 +8,10 @@ let(:avro_message_value) { instance.avro_message_value } let(:avro_message_key) { instance.avro_message_key } + before do + allow(Avromatic.schema_registry).to receive(:register).and_call_original + end + describe "#avro_message_value" do let(:test_class) do Avromatic::Model.model(value_schema_name: 'test.encode_value') @@ -18,6 +22,25 @@ message_value = instance.avro_message_value decoded = test_class.avro_message_decode(message_value) expect(decoded).to eq(instance) + expect(Avromatic.schema_registry).to have_received(:register) + .with('test.encode_value', instance_of(Avro::Schema::RecordSchema)) + end + + context "with a specified value subject" do + let(:test_class) do + Avromatic::Model.model(value_schema_name: 'test.encode_value', + value_schema_subject: 'test.encode_value-subject') + end + let(:values) { { str1: 'a', str2: 'b' } } + + it "encodes the value for the model" do + message_value = instance.avro_message_value + decoded = test_class.avro_message_decode(message_value) + expect(decoded).to eq(instance) + expect(Avromatic.schema_registry).to have_received(:register) + .with('test.encode_value-subject', + instance_of(Avro::Schema::RecordSchema)) + end end context "with a nested record" do @@ -156,6 +179,25 @@ message_key = instance.avro_message_key decoded = test_class.avro_message_decode(message_key, message_value) expect(decoded).to eq(instance) + expect(Avromatic.schema_registry).to have_received(:register) + .with('test.encode_key', instance_of(Avro::Schema::RecordSchema)) + end + + context "with a specified key subject" do + let(:test_class) do + Avromatic::Model.model(value_schema_name: 'test.encode_value', + key_schema_name: 'test.encode_key', + key_schema_subject: 'test.encode_key-subject') + end + + it "encodes the key for the model" do + message_value = instance.avro_message_value + message_key = instance.avro_message_key + decoded = test_class.avro_message_decode(message_key, message_value) + expect(decoded).to eq(instance) + expect(Avromatic.schema_registry).to have_received(:register) + .with('test.encode_key-subject', instance_of(Avro::Schema::RecordSchema)) + end end context "when a model does not have a key schema" do @@ -408,6 +450,22 @@ def self.to_avro(value) it_behaves_like "value schema registration" end + context "a model with a specified subject" do + let(:test_class) do + Avromatic::Model.model(value_schema_name: 'test.encode_value', + value_schema_subject: 'test.encode_value-subject') + end + + it "registers the value schema with the specified subject" do + expect(test_class.register_schemas!).to be_nil + registered = registry.subject_version('test.encode_value-subject') + aggregate_failures do + expect(registered['version']).to eq(1) + expect(registered['schema']).to eq(test_class.value_avro_schema.to_s) + end + end + end + context "a model with a key and value" do let(:test_class) do Avromatic::Model.model(value_schema_name: 'test.encode_value',