avro-patches 0.1.0.rc0
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 +7 -0
- data/.gitignore +9 -0
- data/.overcommit.yml +8 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +57 -0
- data/Rakefile +7 -0
- data/avro-patches.gemspec +37 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/avro-patches.rb +6 -0
- data/lib/avro-patches/logical_types.rb +6 -0
- data/lib/avro-patches/logical_types/io.rb +42 -0
- data/lib/avro-patches/logical_types/logical_types.rb +67 -0
- data/lib/avro-patches/logical_types/schema.rb +112 -0
- data/lib/avro-patches/logical_types/schema_validator.rb +26 -0
- data/lib/avro-patches/schema_compatibility.rb +5 -0
- data/lib/avro-patches/schema_compatibility/io.rb +35 -0
- data/lib/avro-patches/schema_compatibility/schema.rb +69 -0
- data/lib/avro-patches/schema_compatibility/schema_compatibility.rb +145 -0
- data/lib/avro-patches/schema_validator.rb +4 -0
- data/lib/avro-patches/schema_validator/schema.rb +9 -0
- data/lib/avro-patches/schema_validator/schema_validator.rb +194 -0
- data/lib/avro-patches/version.rb +3 -0
- data/lib/avro_patches.rb +1 -0
- metadata +144 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f5f260a70b867406ed25e517f37a3830674d2518
|
4
|
+
data.tar.gz: 3574d8e9dcfc41c7bb33eec6957b9d5f6fd5dc74
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5b063beb14f653c46b0921cb2cfcb99961e63e782167a5f0c66a54875cc8b1e9aa3d953be562829ca02961c3976576e5f20f7c7c5fd0a12ea0905dedfcb820be
|
7
|
+
data.tar.gz: 1e30342006998a39a3f4be96cfe94ada879b94c0b12deeb38656c9fdfe5b3859a0333ea822dd08d3b1d3ed4874a04726df76e771df214892eb87c06ce19c76bc
|
data/.gitignore
ADDED
data/.overcommit.yml
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 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.
|
22
|
+
|
data/README.md
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# avro-patches
|
2
|
+
|
3
|
+
This gem contains patches to the official [Apache Avro](https://avro.apache.org/)
|
4
|
+
Ruby gem v1.8.2.
|
5
|
+
|
6
|
+
The following pending or unreleased changes are included:
|
7
|
+
- [AVRO-1886: Add validation messages](https://github.com/apache/avro/pull/111)
|
8
|
+
- [AVRO-1695: Ruby support for logical types revisited](https://github.com/apache/avro/pull/116)
|
9
|
+
- [AVRO-1969: Add schema compatibility checker for Ruby](https://github.com/apache/avro/pull/170)
|
10
|
+
|
11
|
+
In addition, compatibility with Ruby 2.4 (https://github.com/apache/avro/pull/191)
|
12
|
+
has been integrated with the changes above.
|
13
|
+
|
14
|
+
The following Ruby changes are not included, but could be added in the future:
|
15
|
+
- [AVRO-2001: Adding support for doc attribute](https://github.com/apache/avro/pull/197)
|
16
|
+
- [AVRO-1873: Add CRC32 checksum to Snappy-compressed blocks](https://github.com/apache/avro/pull/121)
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'avro-patches'
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
$ bundle
|
29
|
+
|
30
|
+
Or install it yourself as:
|
31
|
+
|
32
|
+
$ gem install avro-patches
|
33
|
+
|
34
|
+
## Development
|
35
|
+
|
36
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then,
|
37
|
+
run `rake test` to run the tests. You can also run `bin/console` for an
|
38
|
+
interactive prompt that will allow you to experiment.
|
39
|
+
|
40
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
41
|
+
|
42
|
+
To release a new version, update the version number in `version.rb`, and then
|
43
|
+
run `bundle exec rake release`, which will create a git tag for the version,
|
44
|
+
push git commits and tags, and push the `.gem` file to
|
45
|
+
[rubygems.org](https://rubygems.org)
|
46
|
+
.
|
47
|
+
|
48
|
+
## Contributing
|
49
|
+
|
50
|
+
Bug reports and pull requests are welcome on GitHub at
|
51
|
+
https://github.com/salsify/avro-patches.
|
52
|
+
|
53
|
+
## License
|
54
|
+
|
55
|
+
The gem is available as open source under the terms of the
|
56
|
+
[MIT License](http://opensource.org/licenses/MIT).
|
57
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'avro-patches/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'avro-patches'
|
9
|
+
spec.version = AvroPatches::VERSION
|
10
|
+
spec.authors = ['Salsify, Inc']
|
11
|
+
spec.email = ['engineering@salsify.com']
|
12
|
+
|
13
|
+
spec.summary = 'Patches for the official Apache Avro Ruby implementation'
|
14
|
+
spec.description = spec.summary
|
15
|
+
spec.homepage = 'https://github.com/salsify/avro-patches'
|
16
|
+
|
17
|
+
spec.license = 'MIT'
|
18
|
+
|
19
|
+
# Set 'allowed_push_post' to control where this gem can be published.
|
20
|
+
if spec.respond_to?(:metadata)
|
21
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
22
|
+
else
|
23
|
+
raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
|
24
|
+
end
|
25
|
+
|
26
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
27
|
+
spec.bindir = 'bin'
|
28
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = ['lib']
|
30
|
+
|
31
|
+
spec.add_development_dependency 'bundler', '~> 1.15'
|
32
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
33
|
+
spec.add_development_dependency 'test-unit'
|
34
|
+
spec.add_development_dependency 'overcommit'
|
35
|
+
|
36
|
+
spec.add_runtime_dependency 'avro', '1.8.2'
|
37
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'avro-patches'
|
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
data/lib/avro-patches.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
# Changes from "AVRO-1695: Ruby support for logical types revisited"
|
2
|
+
# https://github.com/apache/avro/pull/116
|
3
|
+
require_relative 'logical_types/logical_types'
|
4
|
+
require_relative 'logical_types/schema_validator'
|
5
|
+
require_relative 'logical_types/schema'
|
6
|
+
require_relative 'logical_types/io'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
Avro::IO::DatumWriter.class_eval do
|
2
|
+
def write_data(writers_schema, logical_datum, encoder)
|
3
|
+
datum = writers_schema.type_adapter.encode(logical_datum)
|
4
|
+
|
5
|
+
unless Avro::Schema.validate(writers_schema, datum, encoded = true)
|
6
|
+
raise Avro::IO::AvroTypeError.new(writers_schema, datum)
|
7
|
+
end
|
8
|
+
|
9
|
+
# function dispatch to write datum
|
10
|
+
case writers_schema.type_sym
|
11
|
+
when :null; encoder.write_null(datum)
|
12
|
+
when :boolean; encoder.write_boolean(datum)
|
13
|
+
when :string; encoder.write_string(datum)
|
14
|
+
when :int; encoder.write_int(datum)
|
15
|
+
when :long; encoder.write_long(datum)
|
16
|
+
when :float; encoder.write_float(datum)
|
17
|
+
when :double; encoder.write_double(datum)
|
18
|
+
when :bytes; encoder.write_bytes(datum)
|
19
|
+
when :fixed; write_fixed(writers_schema, datum, encoder)
|
20
|
+
when :enum; write_enum(writers_schema, datum, encoder)
|
21
|
+
when :array; write_array(writers_schema, datum, encoder)
|
22
|
+
when :map; write_map(writers_schema, datum, encoder)
|
23
|
+
when :union; write_union(writers_schema, datum, encoder)
|
24
|
+
when :record, :error, :request; write_record(writers_schema, datum, encoder)
|
25
|
+
else
|
26
|
+
raise Avro::AvroError.new("Unknown type: #{writers_schema.type}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module AvroPatches
|
32
|
+
module LogicalTypes
|
33
|
+
module DatumReaderPatch
|
34
|
+
def read_data(writers_schema, readers_schema, decoder)
|
35
|
+
datum = super
|
36
|
+
readers_schema.type_adapter.decode(datum)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
Avro::IO::DatumReader.prepend(AvroPatches::LogicalTypes::DatumReaderPatch)
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Avro
|
4
|
+
module LogicalTypes
|
5
|
+
module IntDate
|
6
|
+
EPOCH_START = Date.new(1970, 1, 1)
|
7
|
+
|
8
|
+
def self.encode(date)
|
9
|
+
(date - EPOCH_START).to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.decode(int)
|
13
|
+
EPOCH_START + int
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module TimestampMillis
|
18
|
+
def self.encode(value)
|
19
|
+
time = value.to_time
|
20
|
+
time.to_i * 1000 + time.usec / 1000
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.decode(int)
|
24
|
+
s, ms = int / 1000, int % 1000
|
25
|
+
Time.at(s, ms * 1000).utc
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module TimestampMicros
|
30
|
+
def self.encode(value)
|
31
|
+
time = value.to_time
|
32
|
+
time.to_i * 1000_000 + time.usec
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.decode(int)
|
36
|
+
s, us = int / 1000_000, int % 1000_000
|
37
|
+
Time.at(s, us).utc
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module Identity
|
42
|
+
def self.encode(datum)
|
43
|
+
datum
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.decode(datum)
|
47
|
+
datum
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
TYPES = {
|
52
|
+
"int" => {
|
53
|
+
"date" => IntDate
|
54
|
+
},
|
55
|
+
"long" => {
|
56
|
+
"timestamp-millis" => TimestampMillis,
|
57
|
+
"timestamp-micros" => TimestampMicros
|
58
|
+
},
|
59
|
+
}.freeze
|
60
|
+
|
61
|
+
def self.type_adapter(type, logical_type)
|
62
|
+
return unless logical_type
|
63
|
+
|
64
|
+
TYPES.fetch(type, {}).fetch(logical_type, Identity)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
Avro::Schema.class_eval do
|
2
|
+
attr_reader :logical_type
|
3
|
+
|
4
|
+
# Build Avro Schema from data parsed out of JSON string.
|
5
|
+
def self.real_parse(json_obj, names=nil, default_namespace=nil)
|
6
|
+
if json_obj.is_a? Hash
|
7
|
+
type = json_obj['type']
|
8
|
+
logical_type = json_obj['logicalType']
|
9
|
+
raise Avro::Schema::SchemaParseError, %Q(No "type" property: #{json_obj}) if type.nil?
|
10
|
+
|
11
|
+
# Check that the type is valid before calling #to_sym, since symbols are never garbage
|
12
|
+
# collected (important to avoid DoS if we're accepting schemas from untrusted clients)
|
13
|
+
unless Avro::Schema::VALID_TYPES.include?(type)
|
14
|
+
raise Avro::Schema::SchemaParseError, "Unknown type: #{type}"
|
15
|
+
end
|
16
|
+
|
17
|
+
type_sym = type.to_sym
|
18
|
+
if Avro::Schema::PRIMITIVE_TYPES_SYM.include?(type_sym)
|
19
|
+
return Avro::Schema::PrimitiveSchema.new(type_sym, logical_type)
|
20
|
+
|
21
|
+
elsif Avro::Schema::NAMED_TYPES_SYM.include? type_sym
|
22
|
+
name = json_obj['name']
|
23
|
+
namespace = json_obj.include?('namespace') ? json_obj['namespace'] : default_namespace
|
24
|
+
case type_sym
|
25
|
+
when :fixed
|
26
|
+
size = json_obj['size']
|
27
|
+
return Avro::Schema::FixedSchema.new(name, namespace, size, names, logical_type)
|
28
|
+
when :enum
|
29
|
+
symbols = json_obj['symbols']
|
30
|
+
return Avro::Schema::EnumSchema.new(name, namespace, symbols, names)
|
31
|
+
when :record, :error
|
32
|
+
fields = json_obj['fields']
|
33
|
+
return Avro::Schema::RecordSchema.new(name, namespace, fields, names, type_sym)
|
34
|
+
else
|
35
|
+
raise Avro::Schema::SchemaParseError.new("Unknown named type: #{type}")
|
36
|
+
end
|
37
|
+
|
38
|
+
else
|
39
|
+
case type_sym
|
40
|
+
when :array
|
41
|
+
return Avro::Schema::ArraySchema.new(json_obj['items'], names, default_namespace)
|
42
|
+
when :map
|
43
|
+
return Avro::Schema::MapSchema.new(json_obj['values'], names, default_namespace)
|
44
|
+
else
|
45
|
+
raise Avro::Schema::SchemaParseError.new("Unknown Valid Type: #{type}")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
elsif json_obj.is_a? Array
|
50
|
+
# JSON array (union)
|
51
|
+
return Avro::Schema::UnionSchema.new(json_obj, names, default_namespace)
|
52
|
+
elsif Avro::Schema::PRIMITIVE_TYPES.include? json_obj
|
53
|
+
return Avro::Schema::PrimitiveSchema.new(json_obj)
|
54
|
+
else
|
55
|
+
raise Avro::Schema::UnknownSchemaError.new(json_obj)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Determine if a ruby datum is an instance of a schema
|
60
|
+
def self.validate(expected_schema, logical_datum, encoded = false)
|
61
|
+
Avro::SchemaValidator.validate!(expected_schema, logical_datum, encoded)
|
62
|
+
true
|
63
|
+
rescue Avro::SchemaValidator::ValidationError
|
64
|
+
false
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize(type, logical_type=nil)
|
68
|
+
@type_sym = type.is_a?(Symbol) ? type : type.to_sym
|
69
|
+
@logical_type = logical_type
|
70
|
+
end
|
71
|
+
|
72
|
+
def type_adapter
|
73
|
+
@type_adapter ||= Avro::LogicalTypes.type_adapter(type, logical_type) || Avro::LogicalTypes::Identity
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_avro(names=nil)
|
77
|
+
props = {'type' => type}
|
78
|
+
props['logicalType'] = logical_type if logical_type
|
79
|
+
props
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
Avro::Schema::NamedSchema.class_eval do
|
84
|
+
def initialize(type, name, namespace=nil, names=nil, logical_type=nil)
|
85
|
+
super(type, logical_type)
|
86
|
+
@name, @namespace = Avro::Name.extract_namespace(name, namespace)
|
87
|
+
Avro::Name.add_name(names, self)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
Avro::Schema::PrimitiveSchema.class_eval do
|
92
|
+
def initialize(type, logical_type=nil)
|
93
|
+
if Avro::Schema::PRIMITIVE_TYPES_SYM.include?(type)
|
94
|
+
super(type, logical_type)
|
95
|
+
elsif Avro::Schema::PRIMITIVE_TYPES.include?(type)
|
96
|
+
super(type.to_sym, logical_type)
|
97
|
+
else
|
98
|
+
raise Avro::AvroError.new("#{type} is not a valid primitive type.")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
Avro::Schema::FixedSchema.class_eval do
|
104
|
+
def initialize(name, space, size, names=nil, logical_type=nil)
|
105
|
+
# Ensure valid cto args
|
106
|
+
unless size.is_a?(Integer)
|
107
|
+
raise Avro::AvroError, 'Fixed Schema requires a valid integer for size property.'
|
108
|
+
end
|
109
|
+
super(:fixed, name, space, names, logical_type)
|
110
|
+
@size = size
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module AvroPatches
|
2
|
+
module LogicalTypes
|
3
|
+
module SchemaValidatorPatch
|
4
|
+
def validate!(expected_schema, logical_datum, encoded = false)
|
5
|
+
result = Avro::SchemaValidator::Result.new
|
6
|
+
validate_recursive(expected_schema, logical_datum, Avro::SchemaValidator::ROOT_IDENTIFIER, result, encoded)
|
7
|
+
fail Avro::SchemaValidator::ValidationError, result if result.failure?
|
8
|
+
result
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def validate_recursive(expected_schema, logical_datum, path, result, encoded = false)
|
14
|
+
datum = if encoded
|
15
|
+
logical_datum
|
16
|
+
else
|
17
|
+
expected_schema.type_adapter.encode(logical_datum) rescue nil
|
18
|
+
end
|
19
|
+
|
20
|
+
super(expected_schema, datum, path, result)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Avro::SchemaValidator.singleton_class.prepend(AvroPatches::LogicalTypes::SchemaValidatorPatch)
|
@@ -0,0 +1,35 @@
|
|
1
|
+
Avro::IO::DatumReader.class_eval do
|
2
|
+
def self.match_schemas(writers_schema, readers_schema)
|
3
|
+
Avro::SchemaCompatibility.match_schemas(writers_schema, readers_schema)
|
4
|
+
end
|
5
|
+
|
6
|
+
def read_record(writers_schema, readers_schema, decoder)
|
7
|
+
readers_fields_hash = readers_schema.fields_hash
|
8
|
+
read_record = {}
|
9
|
+
writers_schema.fields.each do |field|
|
10
|
+
if readers_field = readers_fields_hash[field.name]
|
11
|
+
field_val = read_data(field.type, readers_field.type, decoder)
|
12
|
+
read_record[field.name] = field_val
|
13
|
+
else
|
14
|
+
skip_data(field.type, decoder)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# fill in the default values
|
19
|
+
if readers_fields_hash.size > read_record.size
|
20
|
+
writers_fields_hash = writers_schema.fields_hash
|
21
|
+
readers_fields_hash.each do |field_name, field|
|
22
|
+
unless writers_fields_hash.has_key? field_name
|
23
|
+
if field.default?
|
24
|
+
field_val = read_default_value(field.type, field.default)
|
25
|
+
read_record[field.name] = field_val
|
26
|
+
else
|
27
|
+
raise Avro::AvroError, "Missing data for #{field.type} with no default"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
read_record
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
Avro::Schema.class_eval do
|
2
|
+
def read?(writers_schema)
|
3
|
+
Avro::SchemaCompatibility.can_read?(writers_schema, self)
|
4
|
+
end
|
5
|
+
|
6
|
+
def be_read?(other_schema)
|
7
|
+
other_schema.read?(self)
|
8
|
+
end
|
9
|
+
|
10
|
+
def mutual_read?(other_schema)
|
11
|
+
Avro::SchemaCompatibility.mutual_read?(other_schema, self)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Avro::Schema::RecordSchema.class_eval do
|
16
|
+
def initialize(name, namespace, fields, names=nil, schema_type=:record)
|
17
|
+
if schema_type == :request || schema_type == 'request'
|
18
|
+
@type_sym = schema_type.to_sym
|
19
|
+
@namespace = namespace
|
20
|
+
else
|
21
|
+
super(schema_type, name, namespace, names)
|
22
|
+
end
|
23
|
+
@fields = if fields
|
24
|
+
self.class.make_field_objects(fields, names, self.namespace)
|
25
|
+
else
|
26
|
+
{}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Avro::Schema::UnionSchema.class_eval do
|
32
|
+
def initialize(schemas, names=nil, default_namespace=nil)
|
33
|
+
super(:union)
|
34
|
+
|
35
|
+
@schemas = schemas.each_with_object([]) do |schema, schema_objects|
|
36
|
+
new_schema = subparse(schema, names, default_namespace)
|
37
|
+
ns_type = new_schema.type_sym
|
38
|
+
|
39
|
+
if Avro::Schema::VALID_TYPES_SYM.include?(ns_type) &&
|
40
|
+
!Avro::Schema::NAMED_TYPES_SYM.include?(ns_type) &&
|
41
|
+
schema_objects.any?{|o| o.type_sym == ns_type }
|
42
|
+
raise Avro::SchemaParseError, "#{ns_type} is already in Union"
|
43
|
+
elsif ns_type == :union
|
44
|
+
raise Avro::SchemaParseError, "Unions cannot contain other unions"
|
45
|
+
else
|
46
|
+
schema_objects << new_schema
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
module AvroPatches
|
54
|
+
module SchemaCompatibility
|
55
|
+
module FieldPatch
|
56
|
+
def default?
|
57
|
+
@default != :no_default
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_avro(names = Set.new)
|
61
|
+
super.tap do |avro|
|
62
|
+
avro['default'] = default if default?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
Avro::Schema::Field.prepend(AvroPatches::SchemaCompatibility::FieldPatch)
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module Avro
|
2
|
+
module SchemaCompatibility
|
3
|
+
def self.can_read?(writers_schema, readers_schema)
|
4
|
+
Checker.new.can_read?(writers_schema, readers_schema)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.mutual_read?(writers_schema, readers_schema)
|
8
|
+
Checker.new.mutual_read?(writers_schema, readers_schema)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.match_schemas(writers_schema, readers_schema)
|
12
|
+
# Note: this does not support aliases!
|
13
|
+
w_type = writers_schema.type_sym
|
14
|
+
r_type = readers_schema.type_sym
|
15
|
+
|
16
|
+
# This conditional is begging for some OO love.
|
17
|
+
if w_type == :union || r_type == :union
|
18
|
+
return true
|
19
|
+
end
|
20
|
+
|
21
|
+
if w_type == r_type
|
22
|
+
return true if Avro::Schema::PRIMITIVE_TYPES_SYM.include?(r_type)
|
23
|
+
|
24
|
+
case r_type
|
25
|
+
when :record
|
26
|
+
return writers_schema.fullname == readers_schema.fullname
|
27
|
+
when :error
|
28
|
+
return writers_schema.fullname == readers_schema.fullname
|
29
|
+
when :request
|
30
|
+
return true
|
31
|
+
when :fixed
|
32
|
+
return writers_schema.fullname == readers_schema.fullname &&
|
33
|
+
writers_schema.size == readers_schema.size
|
34
|
+
when :enum
|
35
|
+
return writers_schema.fullname == readers_schema.fullname
|
36
|
+
when :map
|
37
|
+
return match_schemas(writers_schema.values, readers_schema.values)
|
38
|
+
when :array
|
39
|
+
return match_schemas(writers_schema.items, readers_schema.items)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Handle schema promotion
|
44
|
+
if w_type == :int && [:long, :float, :double].include?(r_type)
|
45
|
+
return true
|
46
|
+
elsif w_type == :long && [:float, :double].include?(r_type)
|
47
|
+
return true
|
48
|
+
elsif w_type == :float && r_type == :double
|
49
|
+
return true
|
50
|
+
elsif w_type == :string && r_type == :bytes
|
51
|
+
return true
|
52
|
+
elsif w_type == :bytes && r_type == :string
|
53
|
+
return true
|
54
|
+
end
|
55
|
+
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
|
59
|
+
class Checker
|
60
|
+
SIMPLE_CHECKS = Avro::Schema::PRIMITIVE_TYPES_SYM.dup.add(:fixed).freeze
|
61
|
+
|
62
|
+
attr_reader :recursion_set
|
63
|
+
private :recursion_set
|
64
|
+
|
65
|
+
def initialize
|
66
|
+
@recursion_set = Set.new
|
67
|
+
end
|
68
|
+
|
69
|
+
def can_read?(writers_schema, readers_schema)
|
70
|
+
full_match_schemas(writers_schema, readers_schema)
|
71
|
+
end
|
72
|
+
|
73
|
+
def mutual_read?(writers_schema, readers_schema)
|
74
|
+
can_read?(writers_schema, readers_schema) && can_read?(readers_schema, writers_schema)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def full_match_schemas(writers_schema, readers_schema)
|
80
|
+
return true if recursion_in_progress?(writers_schema, readers_schema)
|
81
|
+
|
82
|
+
return false unless Avro::SchemaCompatibility.match_schemas(writers_schema, readers_schema)
|
83
|
+
|
84
|
+
if writers_schema.type_sym != :union && SIMPLE_CHECKS.include?(readers_schema.type_sym)
|
85
|
+
return true
|
86
|
+
end
|
87
|
+
|
88
|
+
case readers_schema.type_sym
|
89
|
+
when :record
|
90
|
+
match_record_schemas(writers_schema, readers_schema)
|
91
|
+
when :map
|
92
|
+
full_match_schemas(writers_schema.values, readers_schema.values)
|
93
|
+
when :array
|
94
|
+
full_match_schemas(writers_schema.items, readers_schema.items)
|
95
|
+
when :union
|
96
|
+
match_union_schemas(writers_schema, readers_schema)
|
97
|
+
when :enum
|
98
|
+
# reader's symbols must contain all writer's symbols
|
99
|
+
(writers_schema.symbols - readers_schema.symbols).empty?
|
100
|
+
else
|
101
|
+
if writers_schema.type_sym == :union && writers_schema.schemas.size == 1
|
102
|
+
full_match_schemas(writers_schema.schemas.first, readers_schema)
|
103
|
+
else
|
104
|
+
false
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def match_union_schemas(writers_schema, readers_schema)
|
110
|
+
raise 'readers_schema must be a union' unless readers_schema.type_sym == :union
|
111
|
+
|
112
|
+
case writers_schema.type_sym
|
113
|
+
when :union
|
114
|
+
writers_schema.schemas.all? { |writer_type| full_match_schemas(writer_type, readers_schema) }
|
115
|
+
else
|
116
|
+
readers_schema.schemas.any? { |reader_type| full_match_schemas(writers_schema, reader_type) }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def match_record_schemas(writers_schema, readers_schema)
|
121
|
+
writer_fields_hash = writers_schema.fields_hash
|
122
|
+
readers_schema.fields.each do |field|
|
123
|
+
if writer_fields_hash.key?(field.name)
|
124
|
+
return false unless full_match_schemas(writer_fields_hash[field.name].type, field.type)
|
125
|
+
else
|
126
|
+
return false unless field.default?
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
return true
|
131
|
+
end
|
132
|
+
|
133
|
+
def recursion_in_progress?(writers_schema, readers_schema)
|
134
|
+
key = [writers_schema.object_id, readers_schema.object_id]
|
135
|
+
|
136
|
+
if recursion_set.include?(key)
|
137
|
+
true
|
138
|
+
else
|
139
|
+
recursion_set.add(key)
|
140
|
+
false
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
3
|
+
# distributed with this work for additional information
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
6
|
+
# "License"); you may not use this file except in compliance
|
7
|
+
# with the License. You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module Avro
|
18
|
+
class SchemaValidator
|
19
|
+
ROOT_IDENTIFIER = '.'.freeze
|
20
|
+
PATH_SEPARATOR = '.'.freeze
|
21
|
+
INT_RANGE = Schema::INT_MIN_VALUE..Schema::INT_MAX_VALUE
|
22
|
+
LONG_RANGE = Schema::LONG_MIN_VALUE..Schema::LONG_MAX_VALUE
|
23
|
+
COMPLEX_TYPES = [:array, :error, :map, :record, :request]
|
24
|
+
|
25
|
+
class Result
|
26
|
+
attr_reader :errors
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@errors = []
|
30
|
+
end
|
31
|
+
|
32
|
+
def <<(error)
|
33
|
+
@errors << error
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_error(path, message)
|
37
|
+
self << "at #{path} #{message}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def failure?
|
41
|
+
@errors.any?
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
errors.join("\n")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class ValidationError < StandardError
|
50
|
+
attr_reader :result
|
51
|
+
|
52
|
+
def initialize(result = Result.new)
|
53
|
+
@result = result
|
54
|
+
super
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
result.to_s
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
TypeMismatchError = Class.new(ValidationError)
|
63
|
+
|
64
|
+
class << self
|
65
|
+
def validate!(expected_schema, datum)
|
66
|
+
result = Result.new
|
67
|
+
validate_recursive(expected_schema, datum, ROOT_IDENTIFIER, result)
|
68
|
+
fail ValidationError, result if result.failure?
|
69
|
+
result
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def validate_recursive(expected_schema, datum, path, result)
|
75
|
+
case expected_schema.type_sym
|
76
|
+
when :null
|
77
|
+
fail TypeMismatchError unless datum.nil?
|
78
|
+
when :boolean
|
79
|
+
fail TypeMismatchError unless [true, false].include?(datum)
|
80
|
+
when :string, :bytes
|
81
|
+
fail TypeMismatchError unless datum.is_a?(String)
|
82
|
+
when :int
|
83
|
+
fail TypeMismatchError unless datum.is_a?(Integer)
|
84
|
+
result.add_error(path, "out of bound value #{datum}") unless INT_RANGE.cover?(datum)
|
85
|
+
when :long
|
86
|
+
fail TypeMismatchError unless datum.is_a?(Integer)
|
87
|
+
result.add_error(path, "out of bound value #{datum}") unless LONG_RANGE.cover?(datum)
|
88
|
+
when :float, :double
|
89
|
+
fail TypeMismatchError unless [Float, Integer].any?(&datum.method(:is_a?))
|
90
|
+
when :fixed
|
91
|
+
if datum.is_a? String
|
92
|
+
message = "expected fixed with size #{expected_schema.size}, got \"#{datum}\" with size #{datum.size}"
|
93
|
+
result.add_error(path, message) unless datum.bytesize == expected_schema.size
|
94
|
+
else
|
95
|
+
result.add_error(path, "expected fixed with size #{expected_schema.size}, got #{actual_value_message(datum)}")
|
96
|
+
end
|
97
|
+
when :enum
|
98
|
+
message = "expected enum with values #{expected_schema.symbols}, got #{actual_value_message(datum)}"
|
99
|
+
result.add_error(path, message) unless expected_schema.symbols.include?(datum)
|
100
|
+
when :array
|
101
|
+
validate_array(expected_schema, datum, path, result)
|
102
|
+
when :map
|
103
|
+
validate_map(expected_schema, datum, path, result)
|
104
|
+
when :union
|
105
|
+
validate_union(expected_schema, datum, path, result)
|
106
|
+
when :record, :error, :request
|
107
|
+
fail TypeMismatchError unless datum.is_a?(Hash)
|
108
|
+
expected_schema.fields.each do |field|
|
109
|
+
deeper_path = deeper_path_for_hash(field.name, path)
|
110
|
+
validate_recursive(field.type, datum[field.name], deeper_path, result)
|
111
|
+
end
|
112
|
+
else
|
113
|
+
fail "Unexpected schema type #{expected_schema.type_sym} #{expected_schema.inspect}"
|
114
|
+
end
|
115
|
+
rescue TypeMismatchError
|
116
|
+
result.add_error(path, "expected type #{expected_schema.type_sym}, got #{actual_value_message(datum)}")
|
117
|
+
end
|
118
|
+
|
119
|
+
def validate_array(expected_schema, datum, path, result)
|
120
|
+
fail TypeMismatchError unless datum.is_a?(Array)
|
121
|
+
datum.each_with_index do |d, i|
|
122
|
+
validate_recursive(expected_schema.items, d, path + "[#{i}]", result)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def validate_map(expected_schema, datum, path, result)
|
127
|
+
datum.keys.each do |k|
|
128
|
+
result.add_error(path, "unexpected key type '#{ruby_to_avro_type(k.class)}' in map") unless k.is_a?(String)
|
129
|
+
end
|
130
|
+
datum.each do |k, v|
|
131
|
+
deeper_path = deeper_path_for_hash(k, path)
|
132
|
+
validate_recursive(expected_schema.values, v, deeper_path, result)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def validate_union(expected_schema, datum, path, result)
|
137
|
+
if expected_schema.schemas.size == 1
|
138
|
+
validate_recursive(expected_schema.schemas.first, datum, path, result)
|
139
|
+
return
|
140
|
+
end
|
141
|
+
types_and_results = validate_possible_types(datum, expected_schema, path)
|
142
|
+
failures, successes = types_and_results.partition { |r| r[:result].failure? }
|
143
|
+
return if successes.any?
|
144
|
+
complex_type_failed = failures.detect { |r| COMPLEX_TYPES.include?(r[:type]) }
|
145
|
+
if complex_type_failed
|
146
|
+
complex_type_failed[:result].errors.each { |error| result << error }
|
147
|
+
else
|
148
|
+
types = expected_schema.schemas.map { |s| "'#{s.type_sym}'" }.join(', ')
|
149
|
+
result.add_error(path, "expected union of [#{types}], got #{actual_value_message(datum)}")
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def validate_possible_types(datum, expected_schema, path)
|
154
|
+
expected_schema.schemas.map do |schema|
|
155
|
+
result = Result.new
|
156
|
+
validate_recursive(schema, datum, path, result)
|
157
|
+
{ type: schema.type_sym, result: result }
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def deeper_path_for_hash(sub_key, path)
|
162
|
+
"#{path}#{PATH_SEPARATOR}#{sub_key}".squeeze(PATH_SEPARATOR)
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def actual_value_message(value)
|
168
|
+
avro_type = if value.is_a?(Integer)
|
169
|
+
ruby_integer_to_avro_type(value)
|
170
|
+
else
|
171
|
+
ruby_to_avro_type(value.class)
|
172
|
+
end
|
173
|
+
if value.nil?
|
174
|
+
avro_type
|
175
|
+
else
|
176
|
+
"#{avro_type} with value #{value.inspect}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def ruby_to_avro_type(ruby_class)
|
181
|
+
{
|
182
|
+
NilClass => 'null',
|
183
|
+
String => 'string',
|
184
|
+
Float => 'float',
|
185
|
+
Hash => 'record'
|
186
|
+
}.fetch(ruby_class, ruby_class)
|
187
|
+
end
|
188
|
+
|
189
|
+
def ruby_integer_to_avro_type(value)
|
190
|
+
INT_RANGE.cover?(value) ? 'int' : 'long'
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
data/lib/avro_patches.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'avro-patches'
|
metadata
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: avro-patches
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0.rc0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Salsify, Inc
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-05-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.15'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.15'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: test-unit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: overcommit
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: avro
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.8.2
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.8.2
|
83
|
+
description: Patches for the official Apache Avro Ruby implementation
|
84
|
+
email:
|
85
|
+
- engineering@salsify.com
|
86
|
+
executables:
|
87
|
+
- console
|
88
|
+
- setup
|
89
|
+
extensions: []
|
90
|
+
extra_rdoc_files: []
|
91
|
+
files:
|
92
|
+
- ".gitignore"
|
93
|
+
- ".overcommit.yml"
|
94
|
+
- ".rspec"
|
95
|
+
- ".travis.yml"
|
96
|
+
- CHANGELOG.md
|
97
|
+
- Gemfile
|
98
|
+
- LICENSE.txt
|
99
|
+
- README.md
|
100
|
+
- Rakefile
|
101
|
+
- avro-patches.gemspec
|
102
|
+
- bin/console
|
103
|
+
- bin/setup
|
104
|
+
- lib/avro-patches.rb
|
105
|
+
- lib/avro-patches/logical_types.rb
|
106
|
+
- lib/avro-patches/logical_types/io.rb
|
107
|
+
- lib/avro-patches/logical_types/logical_types.rb
|
108
|
+
- lib/avro-patches/logical_types/schema.rb
|
109
|
+
- lib/avro-patches/logical_types/schema_validator.rb
|
110
|
+
- lib/avro-patches/schema_compatibility.rb
|
111
|
+
- lib/avro-patches/schema_compatibility/io.rb
|
112
|
+
- lib/avro-patches/schema_compatibility/schema.rb
|
113
|
+
- lib/avro-patches/schema_compatibility/schema_compatibility.rb
|
114
|
+
- lib/avro-patches/schema_validator.rb
|
115
|
+
- lib/avro-patches/schema_validator/schema.rb
|
116
|
+
- lib/avro-patches/schema_validator/schema_validator.rb
|
117
|
+
- lib/avro-patches/version.rb
|
118
|
+
- lib/avro_patches.rb
|
119
|
+
homepage: https://github.com/salsify/avro-patches
|
120
|
+
licenses:
|
121
|
+
- MIT
|
122
|
+
metadata:
|
123
|
+
allowed_push_host: https://rubygems.org
|
124
|
+
post_install_message:
|
125
|
+
rdoc_options: []
|
126
|
+
require_paths:
|
127
|
+
- lib
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: 1.3.1
|
138
|
+
requirements: []
|
139
|
+
rubyforge_project:
|
140
|
+
rubygems_version: 2.6.12
|
141
|
+
signing_key:
|
142
|
+
specification_version: 4
|
143
|
+
summary: Patches for the official Apache Avro Ruby implementation
|
144
|
+
test_files: []
|