avro_turf_enchanced 0.7.3

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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +7 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +174 -0
  7. data/Rakefile +2 -0
  8. data/avro_turf.gemspec +30 -0
  9. data/circle.yml +4 -0
  10. data/lib/avro_turf.rb +105 -0
  11. data/lib/avro_turf/cached_schema_registry.rb +26 -0
  12. data/lib/avro_turf/core_ext.rb +10 -0
  13. data/lib/avro_turf/core_ext/date.rb +5 -0
  14. data/lib/avro_turf/core_ext/enumerable.rb +5 -0
  15. data/lib/avro_turf/core_ext/false_class.rb +5 -0
  16. data/lib/avro_turf/core_ext/hash.rb +7 -0
  17. data/lib/avro_turf/core_ext/nil_class.rb +5 -0
  18. data/lib/avro_turf/core_ext/numeric.rb +5 -0
  19. data/lib/avro_turf/core_ext/string.rb +5 -0
  20. data/lib/avro_turf/core_ext/symbol.rb +5 -0
  21. data/lib/avro_turf/core_ext/time.rb +5 -0
  22. data/lib/avro_turf/core_ext/true_class.rb +5 -0
  23. data/lib/avro_turf/messaging.rb +102 -0
  24. data/lib/avro_turf/schema_registry.rb +79 -0
  25. data/lib/avro_turf/schema_store.rb +58 -0
  26. data/lib/avro_turf/schema_to_avro_patch.rb +52 -0
  27. data/lib/avro_turf/test/fake_schema_registry_server.rb +84 -0
  28. data/lib/avro_turf/version.rb +3 -0
  29. data/perf/address.avsc +14 -0
  30. data/perf/encoding_size.rb +26 -0
  31. data/perf/encoding_speed.rb +30 -0
  32. data/perf/person.avsc +14 -0
  33. data/spec/avro_turf_spec.rb +161 -0
  34. data/spec/cached_schema_registry_spec.rb +41 -0
  35. data/spec/core_ext/date_spec.rb +6 -0
  36. data/spec/core_ext/enumerable_spec.rb +12 -0
  37. data/spec/core_ext/false_class_spec.rb +5 -0
  38. data/spec/core_ext/hash_spec.rb +8 -0
  39. data/spec/core_ext/nil_class_spec.rb +5 -0
  40. data/spec/core_ext/numeric_spec.rb +6 -0
  41. data/spec/core_ext/string_spec.rb +5 -0
  42. data/spec/core_ext/symbol_spec.rb +5 -0
  43. data/spec/core_ext/time_spec.rb +6 -0
  44. data/spec/core_ext/true_class_spec.rb +5 -0
  45. data/spec/messaging_spec.rb +112 -0
  46. data/spec/schema_registry_spec.rb +9 -0
  47. data/spec/schema_store_spec.rb +253 -0
  48. data/spec/schema_to_avro_patch_spec.rb +66 -0
  49. data/spec/spec_helper.rb +20 -0
  50. data/spec/support/schema_registry_context.rb +190 -0
  51. metadata +244 -0
@@ -0,0 +1,9 @@
1
+ require 'webmock/rspec'
2
+ require 'avro_turf/schema_registry'
3
+ require 'avro_turf/test/fake_schema_registry_server'
4
+
5
+ describe AvroTurf::SchemaRegistry do
6
+ it_behaves_like "a schema registry client" do
7
+ let(:registry) { described_class.new(registry_url, logger: logger) }
8
+ end
9
+ end
@@ -0,0 +1,253 @@
1
+ require 'avro_turf/schema_store'
2
+
3
+ describe AvroTurf::SchemaStore do
4
+ let(:store) { AvroTurf::SchemaStore.new(path: "spec/schemas") }
5
+
6
+ before do
7
+ FileUtils.mkdir_p("spec/schemas")
8
+ end
9
+
10
+ describe "#find" do
11
+ it "finds schemas on the file system" do
12
+ define_schema "message.avsc", <<-AVSC
13
+ {
14
+ "name": "message",
15
+ "type": "record",
16
+ "fields": [
17
+ {
18
+ "type": "string",
19
+ "name": "message"
20
+ }
21
+ ]
22
+ }
23
+ AVSC
24
+
25
+ schema = store.find("message")
26
+ expect(schema.fullname).to eq "message"
27
+ end
28
+
29
+ it "resolves missing references" do
30
+ define_schema "person.avsc", <<-AVSC
31
+ {
32
+ "name": "person",
33
+ "type": "record",
34
+ "fields": [
35
+ {
36
+ "name": "address",
37
+ "type": "address"
38
+ }
39
+ ]
40
+ }
41
+ AVSC
42
+
43
+ define_schema "address.avsc", <<-AVSC
44
+ {
45
+ "type": "record",
46
+ "name": "address",
47
+ "fields": []
48
+ }
49
+ AVSC
50
+
51
+ schema = store.find("person")
52
+
53
+ expect(schema.fullname).to eq "person"
54
+ end
55
+
56
+ it "finds namespaced schemas" do
57
+ FileUtils.mkdir_p("spec/schemas/test/people")
58
+
59
+ define_schema "test/people/person.avsc", <<-AVSC
60
+ {
61
+ "name": "person",
62
+ "namespace": "test.people",
63
+ "type": "record",
64
+ "fields": [
65
+ {
66
+ "name": "address",
67
+ "type": "test.people.address"
68
+ }
69
+ ]
70
+ }
71
+ AVSC
72
+
73
+ define_schema "test/people/address.avsc", <<-AVSC
74
+ {
75
+ "name": "address",
76
+ "namespace": "test.people",
77
+ "type": "record",
78
+ "fields": []
79
+ }
80
+ AVSC
81
+
82
+ schema = store.find("person", "test.people")
83
+
84
+ expect(schema.fullname).to eq "test.people.person"
85
+ end
86
+
87
+ it "ignores the namespace when the name contains a dot" do
88
+ FileUtils.mkdir_p("spec/schemas/test/acme")
89
+
90
+ define_schema "test/acme/message.avsc", <<-AVSC
91
+ {
92
+ "name": "message",
93
+ "namespace": "test.acme",
94
+ "type": "record",
95
+ "fields": []
96
+ }
97
+ AVSC
98
+
99
+ schema = store.find("test.acme.message", "test.yolo")
100
+
101
+ expect(schema.fullname).to eq "test.acme.message"
102
+ end
103
+
104
+ it "raises AvroTurf::SchemaNotFoundError if there's no schema file matching the name" do
105
+ expect {
106
+ store.find("not_there")
107
+ }.to raise_error(AvroTurf::SchemaNotFoundError, "could not find Avro schema at `spec/schemas/not_there.avsc'")
108
+ end
109
+
110
+ it "raises AvroTurf::SchemaNotFoundError if a type reference cannot be resolved" do
111
+ define_schema "person.avsc", <<-AVSC
112
+ {
113
+ "name": "person",
114
+ "type": "record",
115
+ "fields": [
116
+ {
117
+ "name": "address",
118
+ "type": "address"
119
+ }
120
+ ]
121
+ }
122
+ AVSC
123
+
124
+ expect {
125
+ store.find("person")
126
+ }.to raise_exception(AvroTurf::SchemaNotFoundError)
127
+ end
128
+
129
+ it "raises AvroTurf::SchemaError if the schema's namespace doesn't match the file location" do
130
+ FileUtils.mkdir_p("spec/schemas/test/people")
131
+
132
+ define_schema "test/people/person.avsc", <<-AVSC
133
+ {
134
+ "name": "person",
135
+ "namespace": "yoyoyo.nanana",
136
+ "type": "record",
137
+ "fields": []
138
+ }
139
+ AVSC
140
+
141
+ expect {
142
+ store.find("test.people.person")
143
+ }.to raise_error(AvroTurf::SchemaError, "expected schema `spec/schemas/test/people/person.avsc' to define type `test.people.person'")
144
+ end
145
+
146
+ it "handles circular dependencies" do
147
+ define_schema "a.avsc", <<-AVSC
148
+ {
149
+ "name": "a",
150
+ "type": "record",
151
+ "fields": [
152
+ {
153
+ "type": "b",
154
+ "name": "b"
155
+ }
156
+ ]
157
+ }
158
+ AVSC
159
+
160
+ define_schema "b.avsc", <<-AVSC
161
+ {
162
+ "name": "b",
163
+ "type": "record",
164
+ "fields": [
165
+ {
166
+ "type": "a",
167
+ "name": "a"
168
+ }
169
+ ]
170
+ }
171
+ AVSC
172
+
173
+ schema = store.find("a")
174
+ expect(schema.fullname).to eq "a"
175
+ end
176
+
177
+ it "caches schemas in memory" do
178
+ define_schema "person.avsc", <<-AVSC
179
+ {
180
+ "name": "person",
181
+ "type": "record",
182
+ "fields": [
183
+ {
184
+ "type": "string",
185
+ "name": "full_name"
186
+ }
187
+ ]
188
+ }
189
+ AVSC
190
+
191
+ # Warm the schema cache.
192
+ store.find("person")
193
+
194
+ # Force a failure if the schema file is read again.
195
+ FileUtils.rm("spec/schemas/person.avsc")
196
+
197
+ schema = store.find("person")
198
+ expect(schema.fullname).to eq "person"
199
+ end
200
+ end
201
+
202
+ describe "#load_schemas!" do
203
+ it "loads schemas defined in the `schemas_path` directory" do
204
+ define_schema "person.avsc", <<-AVSC
205
+ {
206
+ "name": "person",
207
+ "type": "record",
208
+ "fields": [
209
+ {
210
+ "type": "string",
211
+ "name": "full_name"
212
+ }
213
+ ]
214
+ }
215
+ AVSC
216
+
217
+ # Warm the schema cache.
218
+ store.load_schemas!
219
+
220
+ # Force a failure if the schema file is read again.
221
+ FileUtils.rm("spec/schemas/person.avsc")
222
+
223
+ schema = store.find("person")
224
+ expect(schema.fullname).to eq "person"
225
+ end
226
+
227
+ it "recursively finds schema definitions in subdirectories" do
228
+ FileUtils.mkdir_p("spec/schemas/foo/bar")
229
+
230
+ define_schema "foo/bar/person.avsc", <<-AVSC
231
+ {
232
+ "name": "foo.bar.person",
233
+ "type": "record",
234
+ "fields": [
235
+ {
236
+ "type": "string",
237
+ "name": "full_name"
238
+ }
239
+ ]
240
+ }
241
+ AVSC
242
+
243
+ # Warm the schema cache.
244
+ store.load_schemas!
245
+
246
+ # Force a failure if the schema file is read again.
247
+ FileUtils.rm("spec/schemas/foo/bar/person.avsc")
248
+
249
+ schema = store.find("foo.bar.person")
250
+ expect(schema.fullname).to eq "foo.bar.person"
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,66 @@
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
25
+
26
+
27
+ describe Avro::IO::DatumReader do
28
+ let(:writer_schema) do
29
+ Avro::Schema.parse <<-AVSC
30
+ {
31
+ "name": "no_default",
32
+ "type": "record",
33
+ "fields": [
34
+ { "type": "string", "name": "one" }
35
+ ]
36
+ }
37
+ AVSC
38
+ end
39
+ let(:reader_schema) do
40
+ Avro::Schema.parse <<-AVSC
41
+ {
42
+ "name": "no_default",
43
+ "type": "record",
44
+ "fields": [
45
+ { "type": "string", "name": "one" },
46
+ { "type": "string", "name": "two" }
47
+ ]
48
+ }
49
+ AVSC
50
+ end
51
+
52
+ it "raises an error for missing fields without a default" do
53
+ stream = StringIO.new
54
+ writer = Avro::IO::DatumWriter.new(writer_schema)
55
+ encoder = Avro::IO::BinaryEncoder.new(stream)
56
+ writer.write({ 'one' => 'first' }, encoder)
57
+ encoded = stream.string
58
+
59
+ stream = StringIO.new(encoded)
60
+ decoder = Avro::IO::BinaryDecoder.new(stream)
61
+ reader = Avro::IO::DatumReader.new(writer_schema, reader_schema)
62
+ expect do
63
+ reader.read(decoder)
64
+ end.to raise_error(Avro::AvroError, 'Missing data for "string" with no default')
65
+ end
66
+ end
@@ -0,0 +1,20 @@
1
+ require 'bundler/setup'
2
+ require 'logger'
3
+ require 'json_spec'
4
+ require 'fakefs/spec_helpers'
5
+ require 'avro_turf'
6
+
7
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
8
+
9
+ module Helpers
10
+ def define_schema(path, content)
11
+ File.open(File.join("spec/schemas", path), "w") do |f|
12
+ f.write(content)
13
+ end
14
+ end
15
+ end
16
+
17
+ RSpec.configure do |config|
18
+ config.include FakeFS::SpecHelpers
19
+ config.include Helpers
20
+ end
@@ -0,0 +1,190 @@
1
+ # This shared example expects a registry variable to be defined
2
+ # with an instance of the registry class being tested.
3
+ shared_examples_for "a schema registry client" do
4
+ let(:logger) { Logger.new(StringIO.new) }
5
+ let(:registry_url) { "http://registry.example.com" }
6
+ let(:subject_name) { "some-subject" }
7
+ let(:schema) do
8
+ {
9
+ type: "record",
10
+ name: "person",
11
+ fields: [
12
+ { name: "name", type: "string" }
13
+ ]
14
+ }.to_json
15
+ end
16
+
17
+ before do
18
+ stub_request(:any, /^#{registry_url}/).to_rack(FakeSchemaRegistryServer)
19
+ FakeSchemaRegistryServer.clear
20
+ end
21
+
22
+ describe "#register and #fetch" do
23
+ it "allows registering a schema" do
24
+ id = registry.register(subject_name, schema)
25
+ fetched_schema = registry.fetch(id)
26
+
27
+ expect(fetched_schema).to eq(schema)
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
49
+ end
50
+
51
+ describe "#fetch" do
52
+ context "when the schema does not exist" do
53
+ it "raises an error" do
54
+ expect do
55
+ registry.fetch(-1)
56
+ end.to raise_error(Excon::Errors::NotFound)
57
+ end
58
+ end
59
+ end
60
+
61
+ describe "#subjects" do
62
+ it "lists the subjects in the registry" do
63
+ subjects = Array.new(2) { |n| "subject#{n}" }
64
+ subjects.each { |subject| registry.register(subject, schema) }
65
+ expect(registry.subjects).to be_json_eql(subjects.to_json)
66
+ end
67
+ end
68
+
69
+ describe "#subject_versions" do
70
+ it "lists all the versions for the subject" do
71
+ 2.times do |n|
72
+ registry.register(subject_name,
73
+ { type: :record, name: "r#{n}", fields: [] }.to_json)
74
+ end
75
+ expect(registry.subject_versions(subject_name))
76
+ .to be_json_eql((1..2).to_a.to_json)
77
+ end
78
+
79
+ context "when the subject does not exist" do
80
+ let(:subject_name) { 'missing' }
81
+
82
+ it "raises an error" do
83
+ expect do
84
+ registry.subject_versions(subject_name).inspect
85
+ end.to raise_error(Excon::Errors::NotFound)
86
+ end
87
+ end
88
+ end
89
+
90
+ describe "#subject_version" do
91
+ before do
92
+ 2.times do |n|
93
+ registry.register(subject_name,
94
+ { type: :record, name: "r#{n}", fields: [] }.to_json)
95
+ end
96
+ end
97
+ let(:expected) do
98
+ {
99
+ name: subject_name,
100
+ version: 1,
101
+ schema: { type: :record, name: "r0", fields: [] }.to_json
102
+ }.to_json
103
+ end
104
+
105
+ it "returns a specific version of a schema" do
106
+ expect(registry.subject_version(subject_name, 1))
107
+ .to eq(JSON.parse(expected))
108
+ end
109
+
110
+ context "when the version is not specified" do
111
+ let(:expected) do
112
+ {
113
+ name: subject_name,
114
+ version: 2,
115
+ schema: { type: :record, name: "r1", fields: [] }.to_json
116
+ }.to_json
117
+ end
118
+
119
+ it "returns the latest version" do
120
+ expect(registry.subject_version(subject_name))
121
+ .to eq(JSON.parse(expected))
122
+ end
123
+ end
124
+
125
+ context "when the subject does not exist" do
126
+ it "raises an error" do
127
+ expect do
128
+ registry.subject_version('missing')
129
+ end.to raise_error(Excon::Errors::NotFound)
130
+ end
131
+ end
132
+
133
+ context "when the version does not exist" do
134
+ it "raises an error" do
135
+ expect do
136
+ registry.subject_version(subject_name, 3)
137
+ end.to raise_error(Excon::Errors::NotFound)
138
+ end
139
+ end
140
+ end
141
+
142
+ describe "#check" do
143
+ context "when the schema exists" do
144
+ let!(:schema_id) { registry.register(subject_name, schema) }
145
+ let(:expected) do
146
+ {
147
+ subject: subject_name,
148
+ id: schema_id,
149
+ version: 1,
150
+ schema: schema
151
+ }.to_json
152
+ end
153
+ it "returns the schema details" do
154
+ expect(registry.check(subject_name, schema)).to eq(JSON.parse(expected))
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
172
+ end
173
+
174
+ context "when the schema is not registered" do
175
+ it "returns nil" do
176
+ expect(registry.check("missing", schema)).to be_nil
177
+ end
178
+ end
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
190
+ end