avro_turf 0.6.2 → 0.7.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f8eb00c2c29b7e52a48030bca31e7bebf9e84b49
4
- data.tar.gz: f651f0a33989a08b6b17b43b75fa493e083954ba
3
+ metadata.gz: 4973e043a05b5317719217586f8cee3b9f913d1a
4
+ data.tar.gz: fb36f804b15c0bed61161d7b73242b7cb0a4cd83
5
5
  SHA512:
6
- metadata.gz: 5011cc4ad6e53bf61d3fb88f3c89c17cb195044d37fa9bf91ec5f30003f3cea59b0edd9e7618aaae57e307aadc2cf000057032d87432fb91a8118ba8609d6f7f
7
- data.tar.gz: 9ffb5c68c4ba50dd19ccf550e3fc861e9ffd7bbaa7117f57f0d7d0a987954f00f95f1385ff6ddf7d5953090623be38f6e2549f89ab1669c81741f66ed7d6292a
6
+ metadata.gz: 234fa5e60fa5d41ead791b6f48c0db08b2ae02a53848b0fbd31dcf13ee07610800c8407a760e86b9329da3f0b0a2d96ea6c44768f7db9438e18aebe72a3f7ab6
7
+ data.tar.gz: deaa4c2afa94cabe4d50070d17c3cd455623461d1fb91397e2ed0a2ff473df9892e409836efe7e4bc13d55658de6b340955c1342a8251bee1a52aef2b8d875cb
data/README.md CHANGED
@@ -101,9 +101,13 @@ The Messaging API will automatically register schemas used for encoding data, an
101
101
 
102
102
  **NOTE:** The Messaging format is _not_ compatible with the Avro data file API.
103
103
 
104
+ The Messaging API is not included by default, so you must require 'avro_turf/messaging' explicitly if you want to use it.
105
+
104
106
  Using the Messaging API is simple once you have set up a Schema Registry service:
105
107
 
106
108
  ```ruby
109
+ require 'avro_turf/messaging'
110
+
107
111
  # You need to pass the URL of your Schema Registry.
108
112
  avro = AvroTurf::Messaging.new(registry_url: "http://my-registry:8081/")
109
113
 
@@ -116,3 +120,55 @@ data = avro.encode({ "title" => "hello, world" }, schema_name: "greeting")
116
120
  # instances of the same schema id will be served by the cache.
117
121
  avro.decode(data) #=> { "title" => "hello, world" }
118
122
  ```
123
+
124
+ In addition to encoding and decoding data, you can check whether a schema is compatible
125
+ with a subject in the registry using the [Compatibility API](http://docs.confluent.io/2.0.0/schema-registry/docs/api.html#compatibility)
126
+
127
+ ```ruby
128
+ require 'avro_turf/messaging'
129
+
130
+ schema = <<-JSON
131
+ {
132
+ "name": "person",
133
+ "type": "record",
134
+ "fields": [
135
+ {
136
+ "name": "full_name",
137
+ "type": "string"
138
+ },
139
+ {
140
+ "name": "address",
141
+ "type": "address"
142
+ }
143
+ ]
144
+ }
145
+ JSON
146
+
147
+ avro = AvroTurf::Messaging.new(registry_url: "http://my-registry:8081/")
148
+
149
+ # Returns true if the schema is compatible, false otherwise.
150
+ avro.compatible?("person", schema)
151
+ ```
152
+
153
+ ### Testing Support
154
+
155
+ AvroTurf includes a `FakeSchemaRegistryServer` that can be used in tests. The
156
+ fake schema registry server depends on Sinatra but it is _not_ listed as a runtime
157
+ dependency for AvroTurf. Sinatra must be added to your Gemfile or gemspec in order
158
+ to use the fake server.
159
+
160
+ Example using RSpec:
161
+
162
+ ```ruby
163
+ require 'avro_turf/test/fake_schema_registry_server'
164
+ require 'webmock/rspec'
165
+
166
+ # within an example
167
+ let(:registry_url) { "http://registry.example.com" }
168
+ before do
169
+ stub_request(:any, /^#{registry_url}/).to_rack(FakeSchemaRegistryServer)
170
+ FakeSchemaRegistryServer.clear
171
+ end
172
+
173
+ # Messaging objects created with the same registry_url will now use the fake server.
174
+ ```
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ["lib"]
19
19
 
20
20
  spec.add_dependency "avro", ">= 1.7.7", "< 1.9"
21
- spec.add_dependency "excon", "~> 0.45.4"
21
+ spec.add_dependency "excon", "~> 0.45"
22
22
 
23
23
  spec.add_development_dependency "bundler", "~> 1.7"
24
24
  spec.add_development_dependency "rake", "~> 10.0"
@@ -3,6 +3,7 @@ require 'avro'
3
3
  require 'json'
4
4
  require 'avro_turf/schema_store'
5
5
  require 'avro_turf/core_ext'
6
+ require 'avro_turf/schema_to_avro_patch'
6
7
 
7
8
  class AvroTurf
8
9
  class Error < StandardError; end
@@ -10,7 +10,7 @@ class AvroTurf::CachedSchemaRegistry
10
10
  end
11
11
 
12
12
  # Delegate the following methods to the upstream
13
- %i(subjects subject_versions subject_version check).each do |name|
13
+ %i(subjects subject_versions subject_version check compatible?).each do |name|
14
14
  define_method(name) do |*args|
15
15
  instance_variable_get(:@upstream).send(name, *args)
16
16
  end
@@ -19,15 +19,19 @@ class AvroTurf
19
19
 
20
20
  # Instantiate a new Messaging instance with the given configuration.
21
21
  #
22
+ # registry - A schema registry object that responds to all methods in the
23
+ # AvroTurf::SchemaRegistry interface.
22
24
  # registry_url - The String URL of the schema registry that should be used.
25
+ # schema_store - A schema store object that responds to #find(schema_name, namespace).
23
26
  # schemas_path - The String file system path where local schemas are stored.
24
27
  # namespace - The String default schema namespace.
25
28
  # logger - The Logger that should be used to log information (optional).
26
- def initialize(registry_url: nil, schemas_path: nil, namespace: nil, logger: nil)
29
+ def initialize(registry: nil, registry_url: nil, schema_store: nil, schemas_path: nil, namespace: nil, logger: nil)
27
30
  @logger = logger || Logger.new($stderr)
28
31
  @namespace = namespace
29
- @schema_store = SchemaStore.new(path: schemas_path || DEFAULT_SCHEMAS_PATH)
30
- @registry = CachedSchemaRegistry.new(SchemaRegistry.new(registry_url, logger: @logger))
32
+ @schema_store = schema_store || SchemaStore.new(path: schemas_path || DEFAULT_SCHEMAS_PATH)
33
+ @registry = registry || CachedSchemaRegistry.new(SchemaRegistry.new(registry_url, logger: @logger))
34
+ @schemas_by_id = {}
31
35
  end
32
36
 
33
37
  # Encodes a message using the specified schema.
@@ -85,8 +89,10 @@ class AvroTurf
85
89
  # The schema id is a 4-byte big-endian integer.
86
90
  schema_id = decoder.read(4).unpack("N").first
87
91
 
88
- writers_schema_json = @registry.fetch(schema_id)
89
- writers_schema = Avro::Schema.parse(writers_schema_json)
92
+ writers_schema = @schemas_by_id.fetch(schema_id) do
93
+ schema_json = @registry.fetch(schema_id)
94
+ @schemas_by_id[schema_id] = Avro::Schema.parse(schema_json)
95
+ end
90
96
 
91
97
  reader = Avro::IO::DatumReader.new(writers_schema, readers_schema)
92
98
  reader.read(decoder)
@@ -51,6 +51,16 @@ class AvroTurf::SchemaRegistry
51
51
  data unless data.has_key?("error_code")
52
52
  end
53
53
 
54
+ # Check if a schema is compatible with the stored version.
55
+ # Returns true if compatible, false otherwise
56
+ # http://docs.confluent.io/2.0.0/schema-registry/docs/api.html#compatibility
57
+ def compatible?(subject, schema, version = 'latest')
58
+ data = post("/compatibility/subjects/#{subject}/versions/#{version}",
59
+ expects: [200, 404],
60
+ body: { schema: schema.to_s }.to_json)
61
+ data.fetch('is_compatible', false) unless data.has_key?('error_code')
62
+ end
63
+
54
64
  private
55
65
 
56
66
  def get(path, **options)
@@ -0,0 +1,41 @@
1
+ class AvroTurf
2
+ module AvroGemPatch
3
+ module RecordSchema
4
+ module ClassMethods
5
+ def make_field_objects(field_data, names, namespace=nil)
6
+ new_field_data = []
7
+ field_data.each do |field|
8
+ if field.respond_to?(:[]) && !field.key?('default')
9
+ field = field.clone
10
+ field['default'] = :no_default
11
+ end
12
+ new_field_data << field
13
+ end
14
+ super(new_field_data, names, namespace)
15
+ end
16
+ end
17
+
18
+ def self.prepended(base)
19
+ class << base
20
+ prepend ClassMethods
21
+ end
22
+ end
23
+ end
24
+
25
+ module Field
26
+ def initialize(type, name, default=:no_default, order=nil, names=nil, namespace=nil)
27
+ super(type, name, default, order, names, namespace)
28
+ end
29
+
30
+ def to_avro(names=Set.new)
31
+ {'name' => name, 'type' => type.to_avro(names)}.tap do |avro|
32
+ avro['default'] = default unless default == :no_default
33
+ avro['order'] = order if order
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ Avro::Schema::RecordSchema.send(:prepend, AvroTurf::AvroGemPatch::RecordSchema)
41
+ Avro::Schema::Field.send(:prepend, AvroTurf::AvroGemPatch::Field)
@@ -1,3 +1,3 @@
1
1
  class AvroTurf
2
- VERSION = "0.6.2"
2
+ VERSION = "0.7.1"
3
3
  end
@@ -1,6 +1,6 @@
1
1
  require 'webmock/rspec'
2
2
  require 'avro_turf/cached_schema_registry'
3
- require_relative 'fake_schema_registry_server'
3
+ require 'avro_turf/test/fake_schema_registry_server'
4
4
 
5
5
  describe AvroTurf::CachedSchemaRegistry do
6
6
  let(:upstream) { instance_double(AvroTurf::SchemaRegistry) }
@@ -1,6 +1,6 @@
1
1
  require 'webmock/rspec'
2
2
  require 'avro_turf/messaging'
3
- require_relative 'fake_schema_registry_server'
3
+ require 'avro_turf/test/fake_schema_registry_server'
4
4
 
5
5
  describe AvroTurf::Messaging do
6
6
  let(:registry_url) { "http://registry.example.com" }
@@ -14,6 +14,8 @@ describe AvroTurf::Messaging do
14
14
  )
15
15
  }
16
16
 
17
+ let(:message) { { "full_name" => "John Doe" } }
18
+
17
19
  before do
18
20
  FileUtils.mkdir_p("spec/schemas")
19
21
  end
@@ -38,42 +40,66 @@ describe AvroTurf::Messaging do
38
40
  AVSC
39
41
  end
40
42
 
41
- it "encodes and decodes messages" do
42
- message = { "full_name" => "John Doe" }
43
- data = avro.encode(message, schema_name: "person")
44
- expect(avro.decode(data)).to eq message
45
- end
43
+ shared_examples_for "encoding and decoding" do
44
+ it "encodes and decodes messages" do
45
+ data = avro.encode(message, schema_name: "person")
46
+ expect(avro.decode(data)).to eq message
47
+ end
46
48
 
47
- it "allows specifying a reader's schema" do
48
- message = { "full_name" => "John Doe" }
49
- data = avro.encode(message, schema_name: "person")
50
- expect(avro.decode(data, schema_name: "person")).to eq message
49
+ it "allows specifying a reader's schema" do
50
+ data = avro.encode(message, schema_name: "person")
51
+ expect(avro.decode(data, schema_name: "person")).to eq message
52
+ end
53
+
54
+ it "caches parsed schemas for decoding" do
55
+ data = avro.encode(message, schema_name: "person")
56
+ avro.decode(data)
57
+ allow(Avro::Schema).to receive(:parse).and_call_original
58
+ expect(avro.decode(data)).to eq message
59
+ expect(Avro::Schema).not_to have_received(:parse)
60
+ end
51
61
  end
52
62
 
53
- context "when active_support/core_ext is present" do
63
+ it_behaves_like "encoding and decoding"
64
+
65
+ context "with a provided registry" do
66
+ let(:registry) { AvroTurf::SchemaRegistry.new(registry_url, logger: logger) }
67
+
54
68
  let(:avro) do
55
- super().tap do |messaging|
56
- # Simulate the presence of active_support/core_ext by monkey patching
57
- # the schema store to monkey patch #to_json on the returned schema.
58
- schema_store = messaging.instance_variable_get(:@schema_store)
59
- def schema_store.find(*)
60
- super.extend(Module.new do
61
- # Replace to_json on the returned schema with an implementation
62
- # that returns something similar to active_support/core_ext/json
63
- def to_json(*args)
64
- instance_variables.each_with_object(Hash.new) do |ivar, result|
65
- result[ivar.to_s.sub('@','')] = instance_variable_get(ivar)
66
- end.to_json(*args)
67
- end
68
- end)
69
- end
70
- end
69
+ AvroTurf::Messaging.new(
70
+ registry: registry,
71
+ schemas_path: "spec/schemas",
72
+ logger: logger
73
+ )
71
74
  end
72
75
 
73
- it "encodes and decodes messages" do
76
+ it_behaves_like "encoding and decoding"
77
+
78
+ it "uses the provided registry" do
79
+ allow(registry).to receive(:register).and_call_original
74
80
  message = { "full_name" => "John Doe" }
75
- data = avro.encode(message, schema_name: "person")
76
- expect(avro.decode(data)).to eq message
81
+ avro.encode(message, schema_name: "person")
82
+ expect(registry).to have_received(:register)
83
+ end
84
+ end
85
+
86
+ context "with a provided schema store" do
87
+ let(:schema_store) { AvroTurf::SchemaStore.new(path: "spec/schemas") }
88
+
89
+ let(:avro) do
90
+ AvroTurf::Messaging.new(
91
+ registry_url: registry_url,
92
+ schema_store: schema_store,
93
+ logger: logger
94
+ )
95
+ end
96
+
97
+ it_behaves_like "encoding and decoding"
98
+
99
+ it "uses the provided schema store" do
100
+ allow(schema_store).to receive(:find).and_call_original
101
+ avro.encode(message, schema_name: "person")
102
+ expect(schema_store).to have_received(:find).with("person", nil)
77
103
  end
78
104
  end
79
105
  end
@@ -1,6 +1,6 @@
1
1
  require 'webmock/rspec'
2
2
  require 'avro_turf/schema_registry'
3
- require_relative 'fake_schema_registry_server'
3
+ require 'avro_turf/test/fake_schema_registry_server'
4
4
 
5
5
  describe AvroTurf::SchemaRegistry do
6
6
  it_behaves_like "a schema registry client" do
@@ -0,0 +1,24 @@
1
+ require 'webmock/rspec'
2
+
3
+ # This spec verifies the monkey-patch that we have to apply until the avro
4
+ # gem releases a fix for bug AVRO-1848:
5
+ # https://issues.apache.org/jira/browse/AVRO-1848
6
+
7
+ describe Avro::Schema do
8
+ it "correctly handles falsey field defaults" do
9
+ schema = Avro::Schema.parse <<-SCHEMA
10
+ {"type": "record", "name": "Record", "namespace": "my.name.space",
11
+ "fields": [
12
+ {"name": "is_usable", "type": "boolean", "default": false}
13
+ ]
14
+ }
15
+ SCHEMA
16
+
17
+ expect(schema.to_avro).to eq({
18
+ 'type' => 'record', 'name' => 'Record', 'namespace' => 'my.name.space',
19
+ 'fields' => [
20
+ {'name' => 'is_usable', 'type' => 'boolean', 'default' => false}
21
+ ]
22
+ })
23
+ end
24
+ end
@@ -26,6 +26,26 @@ shared_examples_for "a schema registry client" do
26
26
 
27
27
  expect(fetched_schema).to eq(schema)
28
28
  end
29
+
30
+ context "with an Avro::Schema" do
31
+ let(:avro_schema) { Avro::Schema.parse(schema) }
32
+
33
+ it "allows registration using an Avro::Schema" do
34
+ id = registry.register(subject_name, avro_schema)
35
+ expect(registry.fetch(id)).to eq(avro_schema.to_s)
36
+ end
37
+
38
+ context "with ActiveSupport present" do
39
+ before do
40
+ break_to_json(avro_schema)
41
+ end
42
+
43
+ it "allows registering an Avro schema" do
44
+ id = registry.register(subject_name, avro_schema)
45
+ expect(registry.fetch(id)).to eq(avro_schema.to_s)
46
+ end
47
+ end
48
+ end
29
49
  end
30
50
 
31
51
  describe "#fetch" do
@@ -133,6 +153,22 @@ shared_examples_for "a schema registry client" do
133
153
  it "returns the schema details" do
134
154
  expect(registry.check(subject_name, schema)).to eq(JSON.parse(expected))
135
155
  end
156
+
157
+ context "with an Avro::Schema" do
158
+ let(:avro_schema) { Avro::Schema.parse(schema) }
159
+
160
+ it "supports a check using an Avro schema" do
161
+ expect(registry.check(subject_name, avro_schema)).to eq(JSON.parse(expected))
162
+ end
163
+
164
+ context "with ActiveSupport present" do
165
+ before { break_to_json(avro_schema) }
166
+
167
+ it "supports a check using an Avro schema" do
168
+ expect(registry.check(subject_name, avro_schema)).to eq(JSON.parse(expected))
169
+ end
170
+ end
171
+ end
136
172
  end
137
173
 
138
174
  context "when the schema is not registered" do
@@ -141,4 +177,14 @@ shared_examples_for "a schema registry client" do
141
177
  end
142
178
  end
143
179
  end
180
+
181
+ # Monkey patch an Avro::Schema to simulate the presence of
182
+ # active_support/core_ext.
183
+ def break_to_json(avro_schema)
184
+ def avro_schema.to_json(*args)
185
+ instance_variables.each_with_object(Hash.new) do |ivar, result|
186
+ result[ivar.to_s.sub('@', '')] = instance_variable_get(ivar)
187
+ end.to_json(*args)
188
+ end
189
+ end
144
190
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: avro_turf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Schierbeck
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-03 00:00:00.000000000 Z
11
+ date: 2016-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: avro
@@ -36,14 +36,14 @@ dependencies:
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.45.4
39
+ version: '0.45'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.45.4
46
+ version: '0.45'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: bundler
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -173,6 +173,8 @@ files:
173
173
  - lib/avro_turf/messaging.rb
174
174
  - lib/avro_turf/schema_registry.rb
175
175
  - lib/avro_turf/schema_store.rb
176
+ - lib/avro_turf/schema_to_avro_patch.rb
177
+ - lib/avro_turf/test/fake_schema_registry_server.rb
176
178
  - lib/avro_turf/version.rb
177
179
  - perf/address.avsc
178
180
  - perf/encoding_size.rb
@@ -190,10 +192,10 @@ files:
190
192
  - spec/core_ext/symbol_spec.rb
191
193
  - spec/core_ext/time_spec.rb
192
194
  - spec/core_ext/true_class_spec.rb
193
- - spec/fake_schema_registry_server.rb
194
195
  - spec/messaging_spec.rb
195
196
  - spec/schema_registry_spec.rb
196
197
  - spec/schema_store_spec.rb
198
+ - spec/schema_to_avro_patch_spec.rb
197
199
  - spec/spec_helper.rb
198
200
  - spec/support/schema_registry_context.rb
199
201
  homepage: https://github.com/dasch/avro_turf
@@ -234,10 +236,10 @@ test_files:
234
236
  - spec/core_ext/symbol_spec.rb
235
237
  - spec/core_ext/time_spec.rb
236
238
  - spec/core_ext/true_class_spec.rb
237
- - spec/fake_schema_registry_server.rb
238
239
  - spec/messaging_spec.rb
239
240
  - spec/schema_registry_spec.rb
240
241
  - spec/schema_store_spec.rb
242
+ - spec/schema_to_avro_patch_spec.rb
241
243
  - spec/spec_helper.rb
242
244
  - spec/support/schema_registry_context.rb
243
245
  has_rdoc: