Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: configurable extra allowable metadata field attributes #73

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,16 @@ Avro::Builder::Rake::AvroGenerateTask.new(name: :custom_gen,
end
```

### Extra metadata attributes

According to the [Avro specification](https://avro.apache.org/docs/1.12.0/specification/) any attribute can be added to a record or field and be used as extra metadata provided it does not clash with the attributes specified.

To enable the use of specific custom metadata attributes on records or fields you can define them with:

```ruby
Avro::Builder.extra_metadata_attributes(:sensitivity, :deprecated_by, :documentation_url)
```

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
6 changes: 6 additions & 0 deletions lib/avro/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ def self.build_schema(str = nil, filename: nil, &block)
def self.add_load_path(*paths)
Avro::Builder::DSL.load_paths.merge(paths)
end

# Define extra allowable metadata attributes for fields
def self.extra_metadata_attributes(*attrs)
Avro::Builder::Field.extra_metadata_attributes(attrs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the spec supports metadata attributes on all JSON object in the serialized Avro schema. Is there a generalized version of this change that supports attributes on constructs other than just fields e.g. records?

Copy link
Author

@hughevans hughevans Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jturkel Yes, I can make that change no problem. Would you rather seperate config blocks for declaring the attributes for for each object type? Or is all together ok?

Avro::Builder.extra_metadata_attributes(:sensitivity_level, :deprecated_by)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with how people are using metadata on Avro schemas but my instinct is to keep it simple for now with a single set of attributes that applies to all types/fields. We can always extend this in the future to support both global and type specific metadata attributes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jturkel Fantastic. I’ve pushed that change. There is the possibility still to have individual setters for records vs fields in the future if you want too.

Avro::Builder::Record.extra_metadata_attributes(attrs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the Avro spec supports metadata on all types rather than just record types. If that's correct, then should this be on Avro::Builder::Types::Type rather than Avro::Builder::Types::RecordType?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jturkel sorry I haven’t got back to this because I hit some code here that could benefit from a refactor. Any possibility we could merge this as is and then circle back later?

end
end
end

Expand Down
15 changes: 10 additions & 5 deletions lib/avro/builder/field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'avro/builder/type_factory'
require 'avro/builder/aliasable'
require 'avro/builder/metadata'

module Avro
module Builder
Expand All @@ -13,6 +14,7 @@ class Field
include Avro::Builder::DslAttributes
include Avro::Builder::Aliasable
include Avro::Builder::AnonymousTypes
include Avro::Builder::Metadata

INTERNAL_ATTRIBUTES = [:optional_field].to_set.freeze

Expand Down Expand Up @@ -84,14 +86,17 @@ def name(value = nil)
end

def serialize(reference_state)
# TODO: order is not included here
{
attrs = {
name: name,
type: serialized_type(reference_state),
doc: doc,
default: default,
aliases: aliases
}.reject { |_, v| v.nil? }.tap do |result|
}

self.class.dsl_attribute_names.each do |attr|
attrs[attr] = send(attr)
end

attrs.reject { |_, v| v.nil? }.tap do |result|
result.merge!(default: nil) if optional_field
end
end
Expand Down
17 changes: 17 additions & 0 deletions lib/avro/builder/metadata.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Avro
module Builder
module Metadata
module ClassMethods
def extra_metadata_attributes(attrs)
dsl_attributes *attrs
end
end

def self.included(base)
base.extend ClassMethods
end
end
end
end
20 changes: 15 additions & 5 deletions lib/avro/builder/types/record_type.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# frozen_string_literal: true

require 'avro/builder/metadata'

module Avro
module Builder
module Types
# This class represents a record in an Avro schema. Records may be defined
# at the top-level or as the type for a field in a record.
class RecordType < Avro::Builder::Types::NamedType
include Avro::Builder::AnonymousTypes
include Avro::Builder::Metadata

DSL_METHODS = [:required, :optional, :extends].to_set.freeze

Expand Down Expand Up @@ -62,15 +65,22 @@ def extends(name, options = {})

def to_h(reference_state = SchemaSerializerReferenceState.new)
reference_state.definition_or_reference(fullname) do
{
attrs = {
type: :record,
name: name,
namespace: namespace,
doc: doc,
aliases: aliases,
logicalType: logical_type,
fields: fields.values.map { |field| field.serialize(reference_state) }
}.reject { |_, v| v.nil? }
}

self.class.dsl_attribute_names.reject do |attr|
[:abstract, :type_name, :type_namespace, :type_aliases, :type_doc].include?(attr)
end.each do |attr|
attrs[attr] = send(attr)
end

attrs[:logicalType] = attrs.delete(:logical_type)

attrs.reject { |_, v| v.nil? }
end
end
alias_method :serialize, :to_h
Expand Down
84 changes: 84 additions & 0 deletions spec/avro/builder/extra_metadata_attributes_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# frozen_string_literal: true

describe Avro::Builder, ".extra_metadata_attributes" do
before do
Avro::Builder.extra_metadata_attributes(:reference, :deprecated_by, :documentation_url)
end

context "applying attributes to fields in a record" do
subject(:schema_json) do
described_class.build do
record :r do
required :n, :null
required :b, :boolean, reference: 'com.example.bool', deprecated_by: 'com.example.bool_v2', other: 'value'
required :s, :string
required :i, :int
optional :l, :long do
doc 'A long value'
order 'ascending'
reference 'com.example.long'
deprecated_by 'com.example.long_v2'
end
required :f, :float
optional :d, :double
required :many_bits, :bytes
end
end
end

let(:expected) do
{
type: :record,
name: :r,
fields: [
{ name: :n, type: :null },
{ name: :b, type: :boolean, reference: 'com.example.bool', deprecated_by: 'com.example.bool_v2' },
{ name: :s, type: :string },
{ name: :i, type: :int },
{
name: :l,
type: [:null, :long],
default: nil,
doc: 'A long value',
order: 'ascending',
reference: 'com.example.long',
deprecated_by: 'com.example.long_v2'
},
{ name: :f, type: :float },
{ name: :d, type: [:null, :double], default: nil },
{ name: :many_bits, type: :bytes }
]
}
end

it { is_expected.to be_json_eql(expected.to_json) }
end

context "applying attributes to the record" do
subject(:schema_json) do
described_class.build do
record :r do
documentation_url 'https://example.com/docs'
reference 'internal-reference'
required :b, :boolean
optional :d, :double
end
end
end

let(:expected) do
{
type: :record,
name: :r,
fields: [
{ name: :b, type: :boolean },
{ name: :d, type: [:null, :double], default: nil }
],
documentation_url: 'https://example.com/docs',
reference: 'internal-reference'
}
end

it { is_expected.to be_json_eql(expected.to_json) }
end
end