avro-builder 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fc0e206e5b0cfe57f356bcc17de31819fc2db7ea
4
+ data.tar.gz: c7ebe9cd48e6ea8a55762d71d1349405e78d022a
5
+ SHA512:
6
+ metadata.gz: b02e6bf7877bb667d2602c871aceda94b5943adbbe356ca80b7479044dab2dbdaa05d130d9f4732dee2314aa87484159195a845c1af354db0855105259fe2e06
7
+ data.tar.gz: b22b39dae5253d9b021fe4deabd8b50ef3757dff483cf0a10ef28612045c32f8ed149db761b68427eb191c8df7751bdabf5e8e98dce88e0b2ccac5180ad9d2a4
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.11.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ # avro-builder changelog
2
+
3
+ ## v0.1.0
4
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in avro-builder.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Salsify, Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # Avro::Builder
2
+
3
+ `Avro::Builder` provides a Ruby DSL to create [Apache Avro](https://avro.apache.org/docs/current/) Schemas.
4
+
5
+ This DSL was created because:
6
+ * The [Avro IDL](https://avro.apache.org/docs/current/idl.html) is not supported in Ruby.
7
+ * The Avro IDL can only be used to define Protocols.
8
+ * Schemas can be extracted as JSON from an IDL Protocol but support
9
+ for imports is still limited.
10
+
11
+ ## Features
12
+ * The syntax is designed for ease-of-use.
13
+ * Definitions can be imported by name. This includes auto-loading from a configured
14
+ set of paths. This allows definitions to split across files and even reused
15
+ between projects.
16
+ * Record definitions can inherit from other record definitions.
17
+
18
+ ## Limitations
19
+
20
+ * Only Avro Schemas, not Protocols are supported.
21
+ * See [Issues](https://github.com/salsify/avro-builder/issues) for functionality
22
+ that has yet to be implemented.
23
+ * This is alpha quality code. There may be breaking changes until version 1.0 is
24
+ released.
25
+
26
+ ## Installation
27
+
28
+ Add this line to your application's Gemfile:
29
+
30
+ ```ruby
31
+ gem 'avro-builder'
32
+ ```
33
+
34
+ And then execute:
35
+
36
+ $ bundle
37
+
38
+ Or install it yourself as:
39
+
40
+ $ gem install avro-builder
41
+
42
+ ## Usage
43
+
44
+ To use `Avro::Builder` define a schema:
45
+
46
+ ```ruby
47
+ namespace 'com.example'
48
+
49
+ fixed :password, 8
50
+
51
+ enum :user_type, :ADMIN, :REGULAR
52
+
53
+ record :user do
54
+ required :id, :long
55
+ required :user_name, :string
56
+ required :type, :user_type, default: :REGULAR
57
+ required :pw, :password
58
+ optional :full_name, :string
59
+ end
60
+ ```
61
+
62
+ The schema definition may be passed as a string or a block to
63
+ `Avro::Builder.build`.
64
+
65
+ This generates the following Avro JSON schema:
66
+ ```json
67
+ {
68
+ "type": "record",
69
+ "name": "user",
70
+ "namespace": "com.example",
71
+ "fields": [
72
+ {
73
+ "name": "id",
74
+ "type": "long"
75
+ },
76
+ {
77
+ "name": "user_name",
78
+ "type": "string"
79
+ },
80
+ {
81
+ "name": "type",
82
+ "type": {
83
+ "name": "user_type",
84
+ "type": "enum",
85
+ "symbols": [
86
+ "ADMIN",
87
+ "REGULAR"
88
+ ],
89
+ "namespace": "com.example"
90
+ },
91
+ "default": "REGULAR"
92
+ },
93
+ {
94
+ "name": "pw",
95
+ "type": {
96
+ "name": "password",
97
+ "type": "fixed",
98
+ "size": 8,
99
+ "namespace": "com.example"
100
+ }
101
+ },
102
+ {
103
+ "name": "full_name",
104
+ "type": [
105
+ "null",
106
+ "string"
107
+ ]
108
+ }
109
+ ]
110
+ }
111
+ ```
112
+
113
+ ### Required and Optional
114
+
115
+ Fields for a record a specified as `required` or `optional`. Optional fields are
116
+ implemented as a union in Avro, where `null` is the first type in the union.
117
+
118
+ ### Named Types
119
+
120
+ `fixed` and `enum` fields may be specified inline as part of a record
121
+ or as standalone named types.
122
+
123
+ ### Auto-loading and Imports
124
+
125
+ Specify paths to search for definitions:
126
+
127
+ ```ruby
128
+ Avro::Builder.add_load_path('/path/to/dsl/files')
129
+ ```
130
+
131
+ Undefined references are automatically loaded from a file with the same name.
132
+ The load paths are searched for `.rb` file with a matching name.
133
+
134
+ Files may also be explicitly imported using `import <filename>`.
135
+
136
+ ### Extends
137
+
138
+ A previously defined record may be referenced in the definition of another
139
+ record using `extends <record_name>`. This adds all of the fields from
140
+ the referenced record to the current record. The current record may override
141
+ fields in the record that it extends.
142
+
143
+ ## Development
144
+
145
+ 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.
146
+
147
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
148
+
149
+ ## Contributing
150
+
151
+ Issues and pull requests are welcome on GitHub at https://github.com/salsify/avro-builder.
152
+
153
+ ## License
154
+
155
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
156
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'avro/builder/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "avro-builder"
8
+ spec.version = Avro::Builder::VERSION
9
+ spec.authors = ["Salsify Engineering"]
10
+ spec.email = ["engineering@salsify.com"]
11
+
12
+ spec.summary = %q{Ruby DSL to create Avro schemas}
13
+ spec.description = spec.summary
14
+ spec.homepage = "https://github.com/salsify/avro-builder.git"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_runtime_dependency "avro", ">= 1.7.0"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.11"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.0"
27
+ spec.add_development_dependency "json_spec"
28
+ spec.add_development_dependency "simplecov"
29
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "avro/builder"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,124 @@
1
+ require 'avro'
2
+ require 'avro/builder/dsl_attributes'
3
+ require 'avro/builder/namespaceable'
4
+ require 'avro/builder/type_factory'
5
+ require 'avro/builder/types'
6
+ require 'avro/builder/field'
7
+ require 'avro/builder/record'
8
+ require 'avro/builder/file_handler'
9
+ require 'avro/builder/schema_serializer_reference_state'
10
+
11
+ module Avro
12
+ module Builder
13
+ # This class is used to construct Avro schemas (not protocols) using a ruby
14
+ # DSL
15
+ class DSL
16
+ include Avro::Builder::DslAttributes
17
+ include Avro::Builder::FileHandler
18
+ include Avro::Builder::TypeFactory
19
+
20
+ dsl_attribute :namespace
21
+
22
+ # An instance of the DSL is initialized with a string or a block to
23
+ # evaluate to define Avro schema objects.
24
+ def initialize(str = nil, &block)
25
+ str ? instance_eval(str) : instance_eval(&block)
26
+ end
27
+
28
+ # Define an Avro schema record
29
+ def record(name, options = {}, &block)
30
+ add_schema_object(build_record(name, options, &block))
31
+ end
32
+
33
+ # Imports from the file with specified name fragment.
34
+ def import(name)
35
+ previous_namespace = namespace
36
+ eval_file(name)
37
+ namespace(previous_namespace)
38
+ end
39
+
40
+ ## DSL methods for Types
41
+
42
+ def enum(name, *symbols, **options, &block)
43
+ type(name, :enum, { symbols: symbols }.merge(options), &block)
44
+ end
45
+
46
+ def fixed(name, size = nil, options = {}, &block)
47
+ type(name, :fixed, { size: size }.merge(options), &block)
48
+ end
49
+
50
+ def type(name, type_name, options = {}, &block)
51
+ build_type(type_name,
52
+ builder: self,
53
+ internal: { name: name, namespace: namespace },
54
+ options: options,
55
+ &block).tap do |type|
56
+ add_schema_object(type)
57
+ end
58
+ end
59
+
60
+ # Lookup an Avro schema object by name, possibly fully qualified by namespace.
61
+ def lookup(key, required: true)
62
+ key_str = key.to_s
63
+ object = schema_objects[key_str]
64
+
65
+ unless object
66
+ import(key)
67
+ object = schema_objects[key_str]
68
+ end
69
+
70
+ raise "Schema object #{key} not found" if required && !object
71
+ object
72
+ rescue
73
+ raise if required
74
+ nil
75
+ end
76
+
77
+ # Return the last schema object processed as a Hash representing
78
+ # the Avro schema.
79
+ def to_h
80
+ @last_object.to_h(SchemaSerializerReferenceState.new)
81
+ end
82
+
83
+ # Return the last schema object processed as an Avro JSON schema
84
+ def to_json(validate: true, pretty: true)
85
+ hash = to_h
86
+ (pretty ? JSON.pretty_generate(hash) : hash.to_json).tap do |json|
87
+ # Parse the schema to validate before returning
88
+ ::Avro::Schema.parse(json) if validate
89
+ end
90
+ end
91
+
92
+ def as_schema
93
+ Avro::Schema.parse(to_json(validate: false))
94
+ end
95
+
96
+ private
97
+
98
+ def builder
99
+ self
100
+ end
101
+
102
+ def schema_objects
103
+ @schema_objects ||= {}
104
+ end
105
+
106
+ def add_schema_object(object)
107
+ @last_object = object
108
+ schema_objects[object.name.to_s] = object
109
+ schema_objects[object.fullname] = object if object.namespace
110
+ end
111
+
112
+ def build_record(name, options, &block)
113
+ Record.new(name, options.merge(namespace: namespace)).tap do |record|
114
+ record.builder = builder
115
+ record.instance_eval(&block)
116
+ end
117
+ end
118
+
119
+ def eval_file(name)
120
+ instance_eval(read_file(name))
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,59 @@
1
+ module Avro
2
+ module Builder
3
+
4
+ # This module provides methods for defining attributes that can be
5
+ # set via the DSL on various objects.
6
+ #
7
+ # The methods generated for DSL attributes are combined getter/setters
8
+ # of the form:
9
+ #
10
+ # attribute(value = nil)
11
+ #
12
+ # When value is provided the attribute is set, and when it is nil the
13
+ # current value is returned.
14
+ #
15
+ # When a DSL attribute is defined, the class also keeps track of the
16
+ # attribute names.
17
+ module DslAttributes
18
+ def self.included(base)
19
+ base.extend ClassMethods
20
+ end
21
+
22
+ def has_dsl_attribute?(name)
23
+ self.class.dsl_attribute_names.include?(name.to_sym)
24
+ end
25
+
26
+ module ClassMethods
27
+ def dsl_attributes(*names)
28
+ names.each do |name|
29
+ dsl_attribute_names << name
30
+ ivar = :"@#{name}"
31
+ define_method(name) do |value = nil|
32
+ value ? instance_variable_set(ivar, value) : instance_variable_get(ivar)
33
+ end
34
+ end
35
+ end
36
+
37
+ # If a block is specified then it is used to define the
38
+ # combined getter/setter method for the DSL attribute.
39
+ def dsl_attribute(name, &block)
40
+ if block_given?
41
+ dsl_attribute_names << name
42
+ define_method(name, &block)
43
+ else
44
+ dsl_attributes(name)
45
+ end
46
+ end
47
+
48
+ def dsl_attribute_names
49
+ @dsl_attribute_names ||=
50
+ if superclass.respond_to?(:dsl_attribute_names)
51
+ superclass.dsl_attribute_names.dup
52
+ else
53
+ Set.new
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,78 @@
1
+ require 'avro/builder/type_factory'
2
+
3
+ module Avro
4
+ module Builder
5
+
6
+ # This class represents a field in a record.
7
+ # A field must be initialized with a type.
8
+ class Field
9
+ include Avro::Builder::DslAttributes
10
+ include Avro::Builder::Namespaceable
11
+ include Avro::Builder::TypeFactory
12
+
13
+ INTERNAL_ATTRIBUTES = Set.new(%i(optional)).freeze
14
+
15
+ attr_accessor :type, :optional, :builder
16
+
17
+ # These attributes may be set as options or via a block in the DSL
18
+ dsl_attributes :doc, :aliases, :default, :order
19
+
20
+ def initialize(name:, type_name:, builder:, internal: {}, options: {}, &block)
21
+ @builder = builder
22
+ @name = name.to_s
23
+
24
+ internal.each do |key, value|
25
+ send("#{key}=", value) if INTERNAL_ATTRIBUTES.include?(key)
26
+ end
27
+
28
+ options.each do |key, value|
29
+ send(key, value) if has_dsl_attribute?(key)
30
+ end
31
+
32
+ @type = builder.lookup(type_name, required: false) ||
33
+ build_type(type_name, field: self, internal: internal, options: options)
34
+
35
+ # DSL calls must be evaluated after the type has been constructed
36
+ instance_eval(&block) if block_given?
37
+ end
38
+
39
+ ## Delegate additional DSL calls to the type
40
+
41
+ def respond_to_missing?(id, include_all = false)
42
+ super || type.respond_to?(id, include_all)
43
+ end
44
+
45
+ def method_missing(id, *args)
46
+ type.respond_to?(id) ? type.send(id, *args) : super
47
+ end
48
+
49
+ # Delegate setting name explicitly via DSL to type
50
+ def name(value = nil)
51
+ if value
52
+ type.name(value)
53
+ else
54
+ # Return the name of the field
55
+ @name
56
+ end
57
+ end
58
+
59
+ def serialize(reference_state)
60
+ {
61
+ name: name,
62
+ type: serialized_type(reference_state),
63
+ doc: doc,
64
+ default: default,
65
+ aliases: aliases
66
+ }.reject { |_, v| v.nil? }
67
+ end
68
+
69
+ private
70
+
71
+ # Optional types must be serialized as an array.
72
+ def serialized_type(reference_state)
73
+ result = type.serialize(reference_state)
74
+ optional ? [:null, result] : result
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,39 @@
1
+ module Avro
2
+ module Builder
3
+ # TODO: eventually this should be refactored into something standalone
4
+ # instead of a module that is included to provide the file handling methods.
5
+ module FileHandler
6
+
7
+ module ClassMethods
8
+ # Load paths are used to search for imports and extends.
9
+ def load_paths
10
+ @load_paths ||= Set.new
11
+ end
12
+ end
13
+
14
+ def self.included(base)
15
+ base.extend ClassMethods
16
+ end
17
+
18
+ def read_file(name)
19
+ File.read(find_file(name))
20
+ end
21
+
22
+ private
23
+
24
+ def find_file(name)
25
+ file_name = "#{name.to_s.sub(/\.rb$/, '')}.rb"
26
+ matches = self.class.load_paths.flat_map do |load_path|
27
+ Dir["#{load_path}/**/*.rb"].select do |file_path|
28
+ file_path.end_with?(file_name)
29
+ end
30
+ end
31
+ raise "Multiple matches: #{matches}" if matches.size > 1
32
+ raise "File not found #{name}" if matches.empty?
33
+
34
+ matches.first
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,12 @@
1
+ module Avro
2
+ module Builder
3
+
4
+ # This concern is used to generate the full name for objects that may
5
+ # be namespaced.
6
+ module Namespaceable
7
+ def fullname
8
+ [namespace, name].compact.join('.')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,84 @@
1
+ module Avro
2
+ module Builder
3
+ # This class represents a record in an Avro schema.
4
+ class Record
5
+ include Avro::Builder::DslAttributes
6
+ include Avro::Builder::Namespaceable
7
+
8
+ attr_accessor :builder
9
+ attr_reader :name
10
+
11
+ dsl_attributes :doc, :aliases, :namespace
12
+
13
+ def initialize(name, options = {})
14
+ @name = name
15
+ options.each do |key, value|
16
+ send(key, value)
17
+ end
18
+ end
19
+
20
+ # Add a required field to the record
21
+ def required(name, type_name, options = {}, &block)
22
+ new_field = Avro::Builder::Field.new(name: name,
23
+ type_name: type_name,
24
+ builder: builder,
25
+ internal: { namespace: namespace },
26
+ options: options,
27
+ &block)
28
+ add_field(new_field)
29
+ end
30
+
31
+ # Add an optional field to the record. In Avro this is represented
32
+ # as a union of null and the type specified here.
33
+ def optional(name, type_name, options = {}, &block)
34
+ new_field = Avro::Builder::Field.new(name: name,
35
+ type_name: type_name,
36
+ builder: builder,
37
+ internal: { namespace: namespace,
38
+ optional: true },
39
+ options: options,
40
+ &block)
41
+ add_field(new_field)
42
+ end
43
+
44
+ # Adds fields from the record with the specified name to the current
45
+ # record.
46
+ def extends(name)
47
+ fields.merge!(builder.lookup(name).duplicated_fields)
48
+ end
49
+
50
+ def to_h(reference_state = SchemaSerializerReferenceState.new)
51
+ reference_state.definition_or_reference(fullname) do
52
+ {
53
+ type: :record,
54
+ name: name,
55
+ namespace: namespace,
56
+ doc: doc,
57
+ aliases: aliases,
58
+ fields: fields.values.map { |field| field.serialize(reference_state) }
59
+ }.reject { |_, v| v.nil? }
60
+ end
61
+ end
62
+
63
+ protected
64
+
65
+ def duplicated_fields
66
+ fields.each_with_object(Hash.new) do |(name, field), result|
67
+ field_copy = field.dup
68
+ result[name] = field_copy
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ # Add field, replacing any existing field with the same name.
75
+ def add_field(field)
76
+ fields[field.name] = field
77
+ end
78
+
79
+ def fields
80
+ @fields ||= {}
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,27 @@
1
+ module Avro
2
+ module Builder
3
+
4
+ # This class is used to keep track of references to each named type while
5
+ # generating an Avro JSON schema. Only the first reference to the type
6
+ # can include all of details of the definition. All subsequent references
7
+ # must use the full name for the type.
8
+ class SchemaSerializerReferenceState
9
+
10
+ attr_reader :references
11
+ private :references
12
+
13
+ def initialize
14
+ @references = Set.new
15
+ end
16
+
17
+ def definition_or_reference(fullname)
18
+ if references.include?(fullname)
19
+ fullname
20
+ else
21
+ references << fullname
22
+ yield
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ module Avro
2
+ module Builder
3
+
4
+ # This concern is used by classes that create new Type instances.
5
+ module TypeFactory
6
+
7
+ private
8
+
9
+ # Return a new Type instance
10
+ def create_type(type_name)
11
+ case
12
+ when Avro::Schema::PRIMITIVE_TYPES_SYM.include?(type_name.to_sym)
13
+ Avro::Builder::Types::Type.new(type_name)
14
+ else
15
+ type_class_name = "#{type_name.to_s.capitalize}Type"
16
+ Avro::Builder::Types.const_get(type_class_name).new
17
+ end
18
+ end
19
+
20
+ # Return a new Type instance, including propagating internal state
21
+ # and setting attributes via the DSL
22
+ def build_type(type_name, field: nil, builder: nil, internal: {}, options: {}, &block)
23
+ create_type(type_name).tap do |type|
24
+ type.field = field
25
+ type.builder = builder
26
+ type.configure_options(internal.merge(options))
27
+ type.instance_eval(&block) if block_given?
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ require 'avro/builder/types/configurable_type'
2
+ require 'avro/builder/types/type_referencer'
3
+
4
+ module Avro
5
+ module Builder
6
+ module Types
7
+ class ArrayType < Type
8
+ include Avro::Builder::Types::SpecificType
9
+ include Avro::Builder::Types::ConfigurableType
10
+ include Avro::Builder::Types::TypeReferencer
11
+
12
+ dsl_attribute :items do |items_type = nil|
13
+ if items_type
14
+ @items = find_or_create_type(items_type)
15
+ else
16
+ @items
17
+ end
18
+ end
19
+
20
+ def serialize(referenced_state)
21
+ {
22
+ type: type_name,
23
+ items: items.serialize(referenced_state)
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ module Avro
2
+ module Builder
3
+ module Types
4
+
5
+ # This concern is used by Types that can be configured using DSL
6
+ # attributes.
7
+ module ConfigurableType
8
+ def configure_options(options = {})
9
+ options.each do |key, value|
10
+ send(key, value) if has_dsl_attribute?(key)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ module Avro
2
+ module Builder
3
+ module Types
4
+ class EnumType < NamedType
5
+
6
+ dsl_attribute :doc
7
+
8
+ dsl_attribute :symbols do |*values|
9
+ # Define symbols explicitly to support values as a splat or single array
10
+ if !values.empty?
11
+ @symbols = values.flatten
12
+ else
13
+ @symbols
14
+ end
15
+ end
16
+
17
+ def serialize(reference_state)
18
+ super(reference_state, overrides: serialized_attributes)
19
+ end
20
+
21
+ def to_h(reference_state)
22
+ super(reference_state, overrides: serialized_attributes)
23
+ end
24
+
25
+ private
26
+
27
+ def serialized_attributes
28
+ { symbols: symbols, doc: doc }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ module Avro
2
+ module Builder
3
+ module Types
4
+ class FixedType < NamedType
5
+
6
+ dsl_attribute :size
7
+
8
+ def serialize(reference_state)
9
+ super(reference_state, overrides: serialized_attributes)
10
+ end
11
+
12
+ def to_h(reference_state)
13
+ super(reference_state, overrides: serialized_attributes)
14
+ end
15
+
16
+ private
17
+
18
+ def serialized_attributes
19
+ { size: size }
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ require 'avro/builder/types/configurable_type'
2
+ require 'avro/builder/types/type_referencer'
3
+
4
+ module Avro
5
+ module Builder
6
+ module Types
7
+ class MapType < Type
8
+ include Avro::Builder::Types::SpecificType
9
+ include Avro::Builder::Types::ConfigurableType
10
+ include Avro::Builder::Types::TypeReferencer
11
+
12
+ dsl_attribute :values do |value_type = nil|
13
+ if value_type
14
+ @values = find_or_create_type(value_type)
15
+ else
16
+ @values
17
+ end
18
+ end
19
+
20
+ def serialize(referenced_state)
21
+ {
22
+ type: type_name,
23
+ values: values.serialize(referenced_state)
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ require 'avro/builder/types/configurable_type'
2
+ require 'avro/builder/namespaceable'
3
+
4
+ module Avro
5
+ module Builder
6
+ module Types
7
+
8
+ # This is an abstract class that represents a type that can be defined
9
+ # with a name, outside a record.
10
+ class NamedType < Type
11
+ include Avro::Builder::Types::SpecificType
12
+ include Avro::Builder::Namespaceable
13
+ include Avro::Builder::Types::ConfigurableType
14
+
15
+ dsl_attributes :name, :namespace, :aliases
16
+
17
+ def generated_name
18
+ name || "__#{field.name}_#{type_name}"
19
+ end
20
+
21
+ # As a type for a field
22
+ # Subclasses may call super with additional overrides to be added
23
+ # to the serialized value.
24
+ def serialize(reference_state, overrides: {})
25
+ reference_state.definition_or_reference(fullname) do
26
+ {
27
+ name: generated_name,
28
+ type: type_name,
29
+ namespace: namespace
30
+ }.merge(overrides).reject { |_, v| v.nil? }
31
+ end
32
+ end
33
+
34
+ # As a top-level, named type
35
+ # Subclasses may call super with additional overrides to be added
36
+ # to the hash representation.
37
+ def to_h(_reference_state, overrides: {})
38
+ {
39
+ name: name,
40
+ type: type_name,
41
+ namespace: namespace,
42
+ aliases: aliases
43
+ }.merge(overrides).reject { |_, v| v.nil? }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ module Avro
2
+ module Builder
3
+ module Types
4
+
5
+ # This module provides common functionality for Types with a specific
6
+ # type name vs the generic Type class.
7
+ module SpecificType
8
+
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ # Override initialize so that type name is not required
14
+ def initialize
15
+ end
16
+
17
+ def type_name
18
+ self.class.type_name
19
+ end
20
+
21
+ module ClassMethods
22
+
23
+ # Infer type_name based on class
24
+ def type_name
25
+ @type_name ||= name.split('::').last.sub('Type', '').downcase.to_sym
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,31 @@
1
+ module Avro
2
+ module Builder
3
+ module Types
4
+ # Base class for simple types. The type name is specified when the
5
+ # type is constructed. The type has no additional attributes, and
6
+ # the type is serialized as just the type name.
7
+ class Type
8
+ include Avro::Builder::DslAttributes
9
+
10
+ attr_reader :type_name
11
+ attr_accessor :field, :builder
12
+
13
+ def initialize(type_name)
14
+ @type_name = type_name
15
+ end
16
+
17
+ def serialize(_reference_state)
18
+ type_name
19
+ end
20
+
21
+ def namespace
22
+ nil
23
+ end
24
+
25
+ def configure_options(_options = {})
26
+ # No-op
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ require 'avro/builder/type_factory'
2
+
3
+ module Avro
4
+ module Builder
5
+ module Types
6
+
7
+ # This concern is used by Types that reference other types.
8
+ module TypeReferencer
9
+ include Avro::Builder::TypeFactory
10
+
11
+ def builder
12
+ (!field.nil? && field.builder) || super
13
+ end
14
+
15
+ def find_or_create_type(type_name)
16
+ builder.lookup(type_name, required: false) || create_type(type_name)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ require 'avro/builder/types/type'
2
+ require 'avro/builder/types/specific_type'
3
+ require 'avro/builder/types/named_type'
4
+ require 'avro/builder/types/enum_type'
5
+ require 'avro/builder/types/fixed_type'
6
+ require 'avro/builder/types/array_type'
7
+ require 'avro/builder/types/map_type'
@@ -0,0 +1,5 @@
1
+ module Avro
2
+ module Builder
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,22 @@
1
+ require 'avro/builder/version'
2
+ require 'avro/builder/dsl'
3
+
4
+ module Avro
5
+ module Builder
6
+
7
+ # Accepts a string or block to eval to define a JSON schema
8
+ def self.build(str = nil, &block)
9
+ Avro::Builder::DSL.new(str, &block).to_json
10
+ end
11
+
12
+ # Accepts a string or block to eval and returns an Avro::Schema
13
+ def self.build_schema(str = nil, &block)
14
+ Avro::Builder::DSL.new(str, &block).as_schema
15
+ end
16
+
17
+ # Add paths that will be searched for definitions
18
+ def self.add_load_path(*paths)
19
+ Avro::Builder::DSL.load_paths.merge(paths)
20
+ end
21
+ end
22
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: avro-builder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Salsify Engineering
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-03-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: avro
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.7.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.7.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.11'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: json_spec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Ruby DSL to create Avro schemas
98
+ email:
99
+ - engineering@salsify.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".travis.yml"
106
+ - CHANGELOG.md
107
+ - Gemfile
108
+ - LICENSE.txt
109
+ - README.md
110
+ - Rakefile
111
+ - avro-builder.gemspec
112
+ - bin/console
113
+ - bin/setup
114
+ - lib/avro/builder.rb
115
+ - lib/avro/builder/dsl.rb
116
+ - lib/avro/builder/dsl_attributes.rb
117
+ - lib/avro/builder/field.rb
118
+ - lib/avro/builder/file_handler.rb
119
+ - lib/avro/builder/namespaceable.rb
120
+ - lib/avro/builder/record.rb
121
+ - lib/avro/builder/schema_serializer_reference_state.rb
122
+ - lib/avro/builder/type_factory.rb
123
+ - lib/avro/builder/types.rb
124
+ - lib/avro/builder/types/array_type.rb
125
+ - lib/avro/builder/types/configurable_type.rb
126
+ - lib/avro/builder/types/enum_type.rb
127
+ - lib/avro/builder/types/fixed_type.rb
128
+ - lib/avro/builder/types/map_type.rb
129
+ - lib/avro/builder/types/named_type.rb
130
+ - lib/avro/builder/types/specific_type.rb
131
+ - lib/avro/builder/types/type.rb
132
+ - lib/avro/builder/types/type_referencer.rb
133
+ - lib/avro/builder/version.rb
134
+ homepage: https://github.com/salsify/avro-builder.git
135
+ licenses:
136
+ - MIT
137
+ metadata: {}
138
+ post_install_message:
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 2.4.8
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: Ruby DSL to create Avro schemas
158
+ test_files: []