avro_turf 1.14.0 → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1c7e87bc8dc94207072e7d09f7ddc844c31b81d3ff46825fa49b5c2548d316c
4
- data.tar.gz: 83b58c9806c7b24096f6c615e5d9e0c260cb1cf37e95e7f87134fbfc8c35bdf1
3
+ metadata.gz: b361914bf3a0900924b82bebf5d61f155834f08ce43b2ba110cf6f7a86e976f5
4
+ data.tar.gz: f3ffe6b3820088e478e047df3ac639f8bc331b7baa829f6069911b0a2b1180da
5
5
  SHA512:
6
- metadata.gz: 4c8e705866a589d86bbfcb44f4715b4fc089968a329fbbdd00b56476f905d096b224d013fb8db115c2c307b438d58e37d3a7be184a46dfdc5bf8cc3d4c9710a5
7
- data.tar.gz: deb82d030abf247b7d1616250d964c6ade5c91fa4e8fcf5247d5157d9d013efe387b3255da0b1364855a28a0a7282de8dd1e29b5e616aba63a5f400400049287
6
+ metadata.gz: f6c4eac8425408913411ab45e25fbbdf94f30c2fd28a273f288dc0bc671d0dd4f65302c880d5eafb4cf7a42bb4c91fd4b9871768644e7fb7ee9344363b1d53ca
7
+ data.tar.gz: '09590e6ab35dbf5099b2fdb3ac800d3e0b78c6b62b0ecfc18ddee5b0cdf98523edbe3c85bd00a1e6730b6c15caad3d65fef9137e67c440d6904d90e79b16738f'
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v1.15.0
6
+
7
+ - Use `default_namespace` from exception to load nested schemas from the correct namespace. (#203)
8
+ - Bump minimum avro version to 1.11.3
9
+ - Add support for schema contexts (#205)
10
+
5
11
  ## v1.14.0
6
12
 
7
13
  - Add `resolv_resolver` parameter to `AvroTurf::Messaging` to make use of custom domain name resolvers and their options, for example `nameserver` and `timeouts` (#202)
data/README.md CHANGED
@@ -153,6 +153,10 @@ By default, AvroTurf will encode data in the Avro data file format. This means t
153
153
 
154
154
  The Messaging API will automatically register schemas used for encoding data, and will fetch the corresponding schema when decoding. Instead of including the full schema in the output, only a schema id generated by the registry is included. Registering the same schema twice is idempotent, so no coordination is needed.
155
155
 
156
+ An optional `schema_context` parameter allows the registry to be scoped to a
157
+ [schema context](https://docs.confluent.io/platform/7.5/schema-registry/schema-linking-cp.html#schema-contexts).
158
+ If there is a need to access multiple contexts, you will need to use multiple instances of `ConfluentSchemaRegistry`.
159
+
156
160
  **NOTE:** [The Messaging format](https://github.com/confluentinc/schema-registry/blob/master/docs/serializer-formatter.rst#wire-format) is _not_ compatible with the Avro data file API.
157
161
 
158
162
  The Messaging API is not included by default, so you must require 'avro_turf/messaging' explicitly if you want to use it.
data/avro_turf.gemspec CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
20
  spec.require_paths = ["lib"]
21
21
 
22
- spec.add_dependency "avro", ">= 1.8.0", "< 1.12"
22
+ spec.add_dependency "avro", ">= 1.11.3", "< 1.12"
23
23
  spec.add_dependency "excon", "~> 0.104"
24
24
 
25
25
  spec.add_development_dependency "bundler", "~> 2.0"
@@ -5,6 +5,7 @@ class AvroTurf::ConfluentSchemaRegistry
5
5
 
6
6
  def initialize(
7
7
  url,
8
+ schema_context: nil,
8
9
  logger: Logger.new($stdout),
9
10
  proxy: nil,
10
11
  user: nil,
@@ -20,6 +21,8 @@ class AvroTurf::ConfluentSchemaRegistry
20
21
  resolv_resolver: nil
21
22
  )
22
23
  @path_prefix = path_prefix
24
+ @schema_context_prefix = schema_context.nil? ? '' : ":.#{schema_context}:"
25
+ @schema_context_options = schema_context.nil? ? {} : {query: {subject: @schema_context_prefix}}
23
26
  @logger = logger
24
27
  headers = Excon.defaults[:headers].merge(
25
28
  "Content-Type" => CONTENT_TYPE
@@ -43,16 +46,16 @@ class AvroTurf::ConfluentSchemaRegistry
43
46
 
44
47
  def fetch(id)
45
48
  @logger.info "Fetching schema with id #{id}"
46
- data = get("/schemas/ids/#{id}", idempotent: true)
49
+ data = get("/schemas/ids/#{id}", idempotent: true, **@schema_context_options, )
47
50
  data.fetch("schema")
48
51
  end
49
52
 
50
53
  def register(subject, schema)
51
- data = post("/subjects/#{subject}/versions", body: { schema: schema.to_s }.to_json)
54
+ data = post("/subjects/#{@schema_context_prefix}#{subject}/versions", body: { schema: schema.to_s }.to_json)
52
55
 
53
56
  id = data.fetch("id")
54
57
 
55
- @logger.info "Registered schema for subject `#{subject}`; id = #{id}"
58
+ @logger.info "Registered schema for subject `#{@schema_context_prefix}#{subject}`; id = #{id}"
56
59
 
57
60
  id
58
61
  end
@@ -64,22 +67,22 @@ class AvroTurf::ConfluentSchemaRegistry
64
67
 
65
68
  # List all versions for a subject
66
69
  def subject_versions(subject)
67
- get("/subjects/#{subject}/versions", idempotent: true)
70
+ get("/subjects/#{@schema_context_prefix}#{subject}/versions", idempotent: true)
68
71
  end
69
72
 
70
73
  # Get a specific version for a subject
71
74
  def subject_version(subject, version = 'latest')
72
- get("/subjects/#{subject}/versions/#{version}", idempotent: true)
75
+ get("/subjects/#{@schema_context_prefix}#{subject}/versions/#{version}", idempotent: true)
73
76
  end
74
77
 
75
78
  # Get the subject and version for a schema id
76
79
  def schema_subject_versions(schema_id)
77
- get("/schemas/ids/#{schema_id}/versions", idempotent: true)
80
+ get("/schemas/ids/#{schema_id}/versions", idempotent: true, **@schema_context_options)
78
81
  end
79
82
 
80
83
  # Check if a schema exists. Returns nil if not found.
81
84
  def check(subject, schema)
82
- data = post("/subjects/#{subject}",
85
+ data = post("/subjects/#{@schema_context_prefix}#{subject}",
83
86
  expects: [200, 404],
84
87
  body: { schema: schema.to_s }.to_json,
85
88
  idempotent: true)
@@ -93,7 +96,7 @@ class AvroTurf::ConfluentSchemaRegistry
93
96
  # - false if incompatible
94
97
  # http://docs.confluent.io/3.1.2/schema-registry/docs/api.html#compatibility
95
98
  def compatible?(subject, schema, version = 'latest')
96
- data = post("/compatibility/subjects/#{subject}/versions/#{version}",
99
+ data = post("/compatibility/subjects/#{@schema_context_prefix}#{subject}/versions/#{version}",
97
100
  expects: [200, 404], body: { schema: schema.to_s }.to_json, idempotent: true)
98
101
  data.fetch('is_compatible', false) unless data.has_key?('error_code')
99
102
  end
@@ -110,12 +113,12 @@ class AvroTurf::ConfluentSchemaRegistry
110
113
 
111
114
  # Get config for subject
112
115
  def subject_config(subject)
113
- get("/config/#{subject}", idempotent: true)
116
+ get("/config/#{@schema_context_prefix}#{subject}", idempotent: true)
114
117
  end
115
118
 
116
119
  # Update config for subject
117
120
  def update_subject_config(subject, config)
118
- put("/config/#{subject}", body: config.to_json, idempotent: true)
121
+ put("/config/#{@schema_context_prefix}#{subject}", body: config.to_json, idempotent: true)
119
122
  end
120
123
 
121
124
  private
@@ -41,6 +41,7 @@ class AvroTurf
41
41
  # registry - A schema registry object that responds to all methods in the
42
42
  # AvroTurf::ConfluentSchemaRegistry interface.
43
43
  # registry_url - The String URL of the schema registry that should be used.
44
+ # schema_context - Schema registry context name (optional)
44
45
  # schema_store - A schema store object that responds to #find(schema_name, namespace).
45
46
  # schemas_path - The String file system path where local schemas are stored.
46
47
  # namespace - The String default schema namespace.
@@ -60,6 +61,7 @@ class AvroTurf
60
61
  def initialize(
61
62
  registry: nil,
62
63
  registry_url: nil,
64
+ schema_context: nil,
63
65
  schema_store: nil,
64
66
  schemas_path: nil,
65
67
  namespace: nil,
@@ -83,6 +85,7 @@ class AvroTurf
83
85
  @registry = registry || CachedConfluentSchemaRegistry.new(
84
86
  ConfluentSchemaRegistry.new(
85
87
  registry_url,
88
+ schema_context: schema_context,
86
89
  logger: @logger,
87
90
  proxy: proxy,
88
91
  user: user,
@@ -74,7 +74,7 @@ class AvroTurf::SchemaStore
74
74
  # Try to first resolve a referenced schema from disk.
75
75
  # If this is successful, the Avro gem will have mutated the
76
76
  # local_schemas_cache, adding all the new schemas it found.
77
- load_schema!(e.type_name, local_schemas_cache)
77
+ load_schema!(::Avro::Name.make_fullname(e.type_name, e.default_namespace), local_schemas_cache)
78
78
 
79
79
  # Attempt to re-parse the original schema now that the dependency
80
80
  # has been resolved and use the now-updated local_schemas_cache to
@@ -1,8 +1,13 @@
1
1
  require 'sinatra/base'
2
2
 
3
3
  class FakeConfluentSchemaRegistryServer < Sinatra::Base
4
- SUBJECTS = Hash.new { Array.new }
5
- SCHEMAS = []
4
+ QUALIFIED_SUBJECT = /
5
+ :(?<context>\.[^:]*)
6
+ :(?<subject>.*)
7
+ /x
8
+ DEFAULT_CONTEXT = '.'
9
+ SUBJECTS = Hash.new { |hash, key| hash[key] = Hash.new { Array.new } }
10
+ SCHEMAS = Hash.new { |hash, key| hash[key] = Array.new }
6
11
  CONFIGS = Hash.new
7
12
  SUBJECT_NOT_FOUND = { error_code: 40401, message: 'Subject not found' }.to_json.freeze
8
13
  VERSION_NOT_FOUND = { error_code: 40402, message: 'Version not found' }.to_json.freeze
@@ -33,17 +38,17 @@ class FakeConfluentSchemaRegistryServer < Sinatra::Base
33
38
  end
34
39
  end
35
40
 
36
- post "/subjects/:subject/versions" do
41
+ post "/subjects/:qualified_subject/versions" do
37
42
  schema = parse_schema
38
- schema_id = SCHEMAS.index(schema)
43
+ context, subject = parse_qualified_subject(params[:qualified_subject])
44
+ schema_id = SCHEMAS[context].index(schema)
39
45
  if schema_id.nil?
40
- SCHEMAS << schema
41
- schema_id = SCHEMAS.size - 1
46
+ SCHEMAS[context] << schema
47
+ schema_id = SCHEMAS[context].size - 1
42
48
  end
43
49
 
44
- subject = params[:subject]
45
- unless SUBJECTS[subject].include?(schema_id)
46
- SUBJECTS[subject] = SUBJECTS[subject] << schema_id
50
+ unless SUBJECTS[context][subject].include?(schema_id)
51
+ SUBJECTS[context][subject] = SUBJECTS[context][subject] << schema_id
47
52
  end
48
53
 
49
54
  { id: schema_id }.to_json
@@ -51,37 +56,46 @@ class FakeConfluentSchemaRegistryServer < Sinatra::Base
51
56
 
52
57
  get "/schemas/ids/:schema_id/versions" do
53
58
  schema_id = params[:schema_id].to_i
54
- schema = SCHEMAS.at(schema_id)
59
+ context, _subject = parse_qualified_subject(params[:subject])
60
+ schema = SCHEMAS[context].at(schema_id)
55
61
  halt(404, SCHEMA_NOT_FOUND) unless schema
56
62
 
57
- related_subjects = SUBJECTS.select {|_, vs| vs.include? schema_id }
63
+ related_subjects = SUBJECTS[context].select {|_, vs| vs.include? schema_id }
58
64
 
59
65
  related_subjects.map do |subject, versions|
60
66
  {
61
- subject: subject,
67
+ subject: qualify_subject(context, subject),
62
68
  version: versions.find_index(schema_id) + 1
63
69
  }
64
70
  end.to_json
65
71
  end
66
72
 
67
73
  get "/schemas/ids/:schema_id" do
68
- schema = SCHEMAS.at(params[:schema_id].to_i)
74
+ context, _subject = parse_qualified_subject(params[:subject])
75
+ schema = SCHEMAS[context].at(params[:schema_id].to_i)
69
76
  halt(404, SCHEMA_NOT_FOUND) unless schema
70
77
  { schema: schema }.to_json
71
78
  end
72
79
 
73
80
  get "/subjects" do
74
- SUBJECTS.keys.to_json
81
+ subject_names = SUBJECTS.reduce([]) do |acc, args|
82
+ context, subjects = args
83
+ subjects.keys.each { |subject| acc << (context == '.' ? subject : ":#{context}:#{subject}") }
84
+ acc
85
+ end
86
+ subject_names.to_json
75
87
  end
76
88
 
77
- get "/subjects/:subject/versions" do
78
- schema_ids = SUBJECTS[params[:subject]]
89
+ get "/subjects/:qualified_subject/versions" do
90
+ context, subject = parse_qualified_subject(params[:qualified_subject])
91
+ schema_ids = SUBJECTS[context][subject]
79
92
  halt(404, SUBJECT_NOT_FOUND) if schema_ids.empty?
80
93
  (1..schema_ids.size).to_a.to_json
81
94
  end
82
95
 
83
- get "/subjects/:subject/versions/:version" do
84
- schema_ids = SUBJECTS[params[:subject]]
96
+ get "/subjects/:qualified_subject/versions/:version" do
97
+ context, subject = parse_qualified_subject(params[:qualified_subject])
98
+ schema_ids = SUBJECTS[context][subject]
85
99
  halt(404, SUBJECT_NOT_FOUND) if schema_ids.empty?
86
100
 
87
101
  schema_id = if params[:version] == 'latest'
@@ -91,29 +105,30 @@ class FakeConfluentSchemaRegistryServer < Sinatra::Base
91
105
  end
92
106
  halt(404, VERSION_NOT_FOUND) unless schema_id
93
107
 
94
- schema = SCHEMAS.at(schema_id)
108
+ schema = SCHEMAS[context].at(schema_id)
95
109
 
96
110
  {
97
- name: params[:subject],
111
+ subject: params[:qualified_subject],
98
112
  version: schema_ids.index(schema_id) + 1,
99
113
  id: schema_id,
100
114
  schema: schema
101
115
  }.to_json
102
116
  end
103
117
 
104
- post "/subjects/:subject" do
118
+ post "/subjects/:qualified_subject" do
105
119
  schema = parse_schema
106
120
 
107
121
  # Note: this does not actually handle the same schema registered under
108
122
  # multiple subjects
109
- schema_id = SCHEMAS.index(schema)
123
+ context, subject = parse_qualified_subject(params[:qualified_subject])
124
+ schema_id = SCHEMAS[context].index(schema)
110
125
 
111
126
  halt(404, SCHEMA_NOT_FOUND) unless schema_id
112
127
 
113
128
  {
114
- subject: params[:subject],
129
+ subject: params[:qualified_subject],
115
130
  id: schema_id,
116
- version: SUBJECTS[params[:subject]].index(schema_id) + 1,
131
+ version: SUBJECTS[context][subject].index(schema_id) + 1,
117
132
  schema: schema
118
133
  }.to_json
119
134
  end
@@ -150,4 +165,19 @@ class FakeConfluentSchemaRegistryServer < Sinatra::Base
150
165
  CONFIGS.clear
151
166
  @global_config = DEFAULT_GLOBAL_CONFIG.dup
152
167
  end
168
+
169
+ private
170
+
171
+ def parse_qualified_subject(qualified_subject)
172
+ match = QUALIFIED_SUBJECT.match(qualified_subject)
173
+ if !match.nil?
174
+ match.named_captures.values_at('context', 'subject')
175
+ else
176
+ [ DEFAULT_CONTEXT, qualified_subject]
177
+ end
178
+ end
179
+
180
+ def qualify_subject(context, subject)
181
+ context == "." ? subject : ":#{context}:#{subject}"
182
+ end
153
183
  end
@@ -1,43 +1,45 @@
1
1
  require 'sinatra/base'
2
2
 
3
3
  class FakePrefixedConfluentSchemaRegistryServer < FakeConfluentSchemaRegistryServer
4
+ DEFAULT_CONTEXT='.'
5
+
4
6
  post "/prefix/subjects/:subject/versions" do
5
7
  schema = parse_schema
6
- ids_for_subject = SUBJECTS[params[:subject]]
8
+ ids_for_subject = SUBJECTS[DEFAULT_CONTEXT][params[:subject]]
7
9
 
8
10
  schemas_for_subject =
9
- SCHEMAS.select
11
+ SCHEMAS[DEFAULT_CONTEXT].select
10
12
  .with_index { |_, i| ids_for_subject.include?(i) }
11
13
 
12
14
  if schemas_for_subject.include?(schema)
13
- schema_id = SCHEMAS.index(schema)
15
+ schema_id = SCHEMAS[DEFAULT_CONTEXT].index(schema)
14
16
  else
15
- SCHEMAS << schema
16
- schema_id = SCHEMAS.size - 1
17
- SUBJECTS[params[:subject]] = SUBJECTS[params[:subject]] << schema_id
17
+ SCHEMAS[DEFAULT_CONTEXT] << schema
18
+ schema_id = SCHEMAS[DEFAULT_CONTEXT].size - 1
19
+ SUBJECTS[DEFAULT_CONTEXT][params[:subject]] = SUBJECTS[DEFAULT_CONTEXT][params[:subject]] << schema_id
18
20
  end
19
21
 
20
22
  { id: schema_id }.to_json
21
23
  end
22
24
 
23
25
  get "/prefix/schemas/ids/:schema_id" do
24
- schema = SCHEMAS.at(params[:schema_id].to_i)
26
+ schema = SCHEMAS[DEFAULT_CONTEXT].at(params[:schema_id].to_i)
25
27
  halt(404, SCHEMA_NOT_FOUND) unless schema
26
28
  { schema: schema }.to_json
27
29
  end
28
30
 
29
31
  get "/prefix/subjects" do
30
- SUBJECTS.keys.to_json
32
+ SUBJECTS[DEFAULT_CONTEXT].keys.to_json
31
33
  end
32
34
 
33
35
  get "/prefix/subjects/:subject/versions" do
34
- schema_ids = SUBJECTS[params[:subject]]
36
+ schema_ids = SUBJECTS[DEFAULT_CONTEXT][params[:subject]]
35
37
  halt(404, SUBJECT_NOT_FOUND) if schema_ids.empty?
36
38
  (1..schema_ids.size).to_a.to_json
37
39
  end
38
40
 
39
41
  get "/prefix/subjects/:subject/versions/:version" do
40
- schema_ids = SUBJECTS[params[:subject]]
42
+ schema_ids = SUBJECTS[DEFAULT_CONTEXT][params[:subject]]
41
43
  halt(404, SUBJECT_NOT_FOUND) if schema_ids.empty?
42
44
 
43
45
  schema_id = if params[:version] == 'latest'
@@ -47,7 +49,7 @@ class FakePrefixedConfluentSchemaRegistryServer < FakeConfluentSchemaRegistrySer
47
49
  end
48
50
  halt(404, VERSION_NOT_FOUND) unless schema_id
49
51
 
50
- schema = SCHEMAS.at(schema_id)
52
+ schema = SCHEMAS[DEFAULT_CONTEXT].at(schema_id)
51
53
 
52
54
  {
53
55
  name: params[:subject],
@@ -69,7 +71,7 @@ class FakePrefixedConfluentSchemaRegistryServer < FakeConfluentSchemaRegistrySer
69
71
  {
70
72
  subject: params[:subject],
71
73
  id: schema_id,
72
- version: SUBJECTS[params[:subject]].index(schema_id) + 1,
74
+ version: SUBJECTS[DEFAULT_CONTEXT][params[:subject]].index(schema_id) + 1,
73
75
  schema: schema
74
76
  }.to_json
75
77
  end
@@ -1,3 +1,3 @@
1
1
  class AvroTurf
2
- VERSION = "1.14.0"
2
+ VERSION = "1.15.0"
3
3
  end
@@ -10,36 +10,56 @@ describe AvroTurf::ConfluentSchemaRegistry do
10
10
  let(:client_key_pass) { "test client key password" }
11
11
  let(:connect_timeout) { 10 }
12
12
 
13
- it_behaves_like "a confluent schema registry client" do
14
- let(:registry) {
15
- described_class.new(
16
- registry_url,
17
- logger: logger,
18
- client_cert: client_cert,
19
- client_key: client_key,
20
- client_key_pass: client_key_pass
21
- )
22
- }
13
+ context 'authenticated by cert' do
14
+ it_behaves_like "a confluent schema registry client" do
15
+ let(:registry) {
16
+ described_class.new(
17
+ registry_url,
18
+ logger: logger,
19
+ client_cert: client_cert,
20
+ client_key: client_key,
21
+ client_key_pass: client_key_pass
22
+ )
23
+ }
24
+ end
23
25
  end
24
26
 
25
- it_behaves_like "a confluent schema registry client" do
26
- let(:registry) {
27
- described_class.new(
28
- registry_url,
29
- user: user,
30
- password: password,
31
- )
32
- }
27
+ context 'authenticated by basic auth' do
28
+ it_behaves_like "a confluent schema registry client" do
29
+ let(:registry) {
30
+ described_class.new(
31
+ registry_url,
32
+ user: user,
33
+ password: password,
34
+ )
35
+ }
36
+ end
33
37
  end
34
38
 
35
- it_behaves_like "a confluent schema registry client" do
36
- let(:registry) {
37
- described_class.new(
38
- registry_url,
39
- user: user,
40
- password: password,
41
- connect_timeout: connect_timeout
42
- )
43
- }
39
+ context 'with connect_timeout' do
40
+ it_behaves_like "a confluent schema registry client" do
41
+ let(:registry) {
42
+ described_class.new(
43
+ registry_url,
44
+ user: user,
45
+ password: password,
46
+ connect_timeout: connect_timeout
47
+ )
48
+ }
49
+ end
50
+ end
51
+
52
+ context 'with non default schema_context' do
53
+ it_behaves_like "a confluent schema registry client", schema_context: 'other' do
54
+ let(:registry) {
55
+ described_class.new(
56
+ registry_url,
57
+ schema_context: 'other',
58
+ user: user,
59
+ password: password,
60
+ connect_timeout: connect_timeout
61
+ )
62
+ }
63
+ end
44
64
  end
45
65
  end
@@ -26,6 +26,35 @@ describe AvroTurf::SchemaStore do
26
26
  expect(schema.fullname).to eq "message"
27
27
  end
28
28
 
29
+ it 'searches for nested types with the correct namespace' do
30
+ define_schema "foo/bar.avsc", <<-AVSC
31
+ {
32
+ "type": "record",
33
+ "namespace": "foo",
34
+ "name": "bar",
35
+ "fields": [
36
+ {
37
+ "name": "another",
38
+ "type": "another_schema"
39
+ }
40
+ ]
41
+ }
42
+ AVSC
43
+
44
+ define_schema "foo/another_schema.avsc", <<-AVSC
45
+ {
46
+ "namespace": "foo",
47
+ "name": "another_schema",
48
+ "type": "record",
49
+ "fields": [ { "name": "str", "type": "string" } ]
50
+ }
51
+ AVSC
52
+
53
+ schema = store.find('foo.bar')
54
+ expect(schema.fullname).to eq "foo.bar"
55
+ expect(schema.fields.first.type.fullname).to eq "foo.another_schema"
56
+ end
57
+
29
58
  it "resolves missing references when nested schema is not a named type" do
30
59
  define_schema "root.avsc", <<-AVSC
31
60
  {
data/spec/spec_helper.rb CHANGED
@@ -9,7 +9,10 @@ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
9
9
 
10
10
  module Helpers
11
11
  def define_schema(path, content)
12
- File.open(File.join("spec/schemas", path), "w") do |f|
12
+ file = File.join("spec/schemas", path)
13
+ dir = File.dirname(file)
14
+ FileUtils.mkdir_p(dir)
15
+ File.open(file, "w") do |f|
13
16
  f.write(content)
14
17
  end
15
18
  end
@@ -1,9 +1,10 @@
1
1
  # This shared example expects a registry variable to be defined
2
2
  # with an instance of the registry class being tested.
3
- shared_examples_for "a confluent schema registry client" do
3
+ shared_examples_for "a confluent schema registry client" do |schema_context: nil|
4
4
  let(:logger) { Logger.new(StringIO.new) }
5
5
  let(:registry_url) { "http://registry.example.com" }
6
6
  let(:subject_name) { "some-subject" }
7
+ let(:expected_subject_name) { with_schema_context_if_applicable(schema_context, subject_name) }
7
8
  let(:schema) do
8
9
  {
9
10
  type: "record",
@@ -72,8 +73,9 @@ shared_examples_for "a confluent schema registry client" do
72
73
  describe "#subjects" do
73
74
  it "lists the subjects in the registry" do
74
75
  subjects = Array.new(2) { |n| "subject#{n}" }
76
+ expected_subjects = subjects.map { |subject| with_schema_context_if_applicable(schema_context, subject) }
75
77
  subjects.each { |subject| registry.register(subject, schema) }
76
- expect(registry.subjects).to be_json_eql(subjects.to_json)
78
+ expect(registry.subjects).to be_json_eql(expected_subjects.to_json)
77
79
  end
78
80
  end
79
81
 
@@ -81,16 +83,17 @@ shared_examples_for "a confluent schema registry client" do
81
83
  it "returns subject and version for a schema id" do
82
84
  schema_id1 = registry.register(subject_name, { type: :record, name: "r1", fields: [] }.to_json)
83
85
  registry.register(subject_name, { type: :record, name: "r2", fields: [] }.to_json)
84
- schema_id2 = registry.register("other#{subject_name}", { type: :record, name: "r2", fields: [] }.to_json)
86
+ other_subject_name = "other#{subject_name}"
87
+ schema_id2 = registry.register(other_subject_name, { type: :record, name: "r2", fields: [] }.to_json)
85
88
  expect(registry.schema_subject_versions(schema_id1)).to eq([
86
- 'subject' => subject_name,
89
+ 'subject' => expected_subject_name,
87
90
  'version' => 1
88
91
  ])
89
92
  expect(registry.schema_subject_versions(schema_id2)).to include({
90
- 'subject' => subject_name,
93
+ 'subject' => expected_subject_name,
91
94
  'version' => 2
92
95
  },{
93
- 'subject' => "other#{subject_name}",
96
+ 'subject' => with_schema_context_if_applicable(schema_context, other_subject_name),
94
97
  'version' => 1
95
98
  } )
96
99
  end
@@ -135,7 +138,7 @@ shared_examples_for "a confluent schema registry client" do
135
138
 
136
139
  let(:expected) do
137
140
  {
138
- name: subject_name,
141
+ subject: expected_subject_name,
139
142
  version: 1,
140
143
  id: schema_id1,
141
144
  schema: { type: :record, name: "r0", fields: [] }.to_json
@@ -150,7 +153,7 @@ shared_examples_for "a confluent schema registry client" do
150
153
  context "when the version is not specified" do
151
154
  let(:expected) do
152
155
  {
153
- name: subject_name,
156
+ subject: expected_subject_name,
154
157
  version: 2,
155
158
  id: schema_id2,
156
159
  schema: { type: :record, name: "r1", fields: [] }.to_json
@@ -185,7 +188,7 @@ shared_examples_for "a confluent schema registry client" do
185
188
  let!(:schema_id) { registry.register(subject_name, schema) }
186
189
  let(:expected) do
187
190
  {
188
- subject: subject_name,
191
+ subject: expected_subject_name,
189
192
  id: schema_id,
190
193
  version: 1,
191
194
  schema: schema
@@ -289,4 +292,8 @@ shared_examples_for "a confluent schema registry client" do
289
292
  end.to_json(*args)
290
293
  end
291
294
  end
295
+
296
+ def with_schema_context_if_applicable(schema_context, subject_name)
297
+ schema_context.nil? ? subject_name : ":.#{schema_context}:#{subject_name}"
298
+ end
292
299
  end
@@ -6,66 +6,55 @@ describe FakeConfluentSchemaRegistryServer do
6
6
 
7
7
  def app; described_class; end
8
8
 
9
- let(:schema) do
10
- {
11
- type: "record",
12
- name: "person",
13
- fields: [
14
- { name: "name", type: "string" }
15
- ]
16
- }.to_json
17
- end
18
-
19
9
  describe 'POST /subjects/:subject/versions' do
20
10
  it 'returns the same schema ID when invoked with same schema and same subject' do
21
- post '/subjects/person/versions', { schema: schema }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
11
+ post '/subjects/person/versions', { schema: schema(name: "person") }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
22
12
 
23
13
  expected_id = JSON.parse(last_response.body).fetch('id')
24
14
 
25
- post '/subjects/person/versions', { schema: schema }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
15
+ post '/subjects/person/versions', { schema: schema(name: "person") }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
26
16
 
27
17
  expect(JSON.parse(last_response.body).fetch('id')).to eq expected_id
28
18
  end
29
19
 
30
20
  it 'returns the same schema ID when invoked with same schema and different subject' do
31
- post '/subjects/person/versions', { schema: schema }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
21
+ post '/subjects/person/versions', { schema: schema(name: "person") }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
32
22
 
33
23
  original_id = JSON.parse(last_response.body).fetch('id')
34
24
 
35
- post '/subjects/happy-person/versions', { schema: schema }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
25
+ post '/subjects/happy-person/versions', { schema: schema(name: "person") }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
36
26
 
37
27
  expect(JSON.parse(last_response.body).fetch('id')).to eq original_id
38
28
  end
39
29
 
40
30
  it 'returns a different schema ID when invoked with a different schema' do
41
- post '/subjects/person/versions', { schema: schema }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
31
+ post '/subjects/person/versions', { schema: schema(name: "person") }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
42
32
 
43
33
  original_id = JSON.parse(last_response.body).fetch('id')
44
34
 
45
- other_schema = {
46
- type: "record",
47
- name: "other",
48
- fields: [
49
- { name: "name", type: "string" }
50
- ]
51
- }.to_json
52
-
53
- post '/subjects/person/versions', { schema: other_schema }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
35
+ post '/subjects/person/versions', { schema: schema(name: "other") }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
54
36
 
55
37
  expect(JSON.parse(last_response.body).fetch('id')).to_not eq original_id
56
38
  end
39
+
40
+ context 'with a clean registry' do
41
+ before do
42
+ FakeConfluentSchemaRegistryServer.clear
43
+ end
44
+
45
+ it 'assigns same schema id for different schemas in different contexts' do
46
+ post '/subjects/:.context1:cats/versions', { schema: schema(name: 'name1') }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
47
+ schema1_id = JSON.parse(last_response.body).fetch('id') # Original cats schema
48
+
49
+ post '/subjects/:.context2:dogs/versions', { schema: schema(name: 'name2') }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
50
+ schema2_id = JSON.parse(last_response.body).fetch('id') # Original cats schema
51
+
52
+ expect(schema1_id).to eq(schema2_id)
53
+ end
54
+ end
57
55
  end
58
56
 
59
57
  describe 'GET /schemas/ids/:id/versions' do
60
- def schema(name:)
61
- {
62
- type: "record",
63
- name: name,
64
- fields: [
65
- { name: "name", type: "string" },
66
- ]
67
- }.to_json
68
- end
69
58
 
70
59
  it "returns array containing subjects and versions for given schema id" do
71
60
  schema1 = schema(name: "name1")
@@ -97,5 +86,143 @@ describe FakeConfluentSchemaRegistryServer do
97
86
  'version' => 1
98
87
  })
99
88
  end
89
+
90
+ describe 'schema registry contexts' do
91
+ it 'allows different schemas to have same schema version', :aggregate_failures do
92
+ petSchema = schema(name: 'pet_cat')
93
+ animalSchema = schema(name: 'animal_cat')
94
+
95
+ post '/subjects/:.pets:cats/versions', { schema: petSchema }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
96
+ pet_id = JSON.parse(last_response.body).fetch('id') # Context1 cats schema
97
+
98
+ post '/subjects/:.animals:cats/versions', { schema: animalSchema }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
99
+ animal_id = JSON.parse(last_response.body).fetch('id') # Context2 cats schema
100
+
101
+ get "/schemas/ids/#{pet_id}/versions?subject=:.pets:"
102
+ result = JSON.parse(last_response.body)
103
+
104
+ expect(result).to eq [{
105
+ 'subject' => ':.pets:cats',
106
+ 'version' => 1
107
+ }]
108
+
109
+ get "/schemas/ids/#{animal_id}/versions?subject=:.animals:"
110
+ result = JSON.parse(last_response.body)
111
+
112
+ expect(result).to eq [{
113
+ 'subject' => ':.animals:cats',
114
+ 'version' => 1
115
+ }]
116
+ end
117
+ end
118
+ end
119
+
120
+ describe 'GET /schemas/ids/:schema_id' do
121
+ it 'returns schema by id', :aggregate_failures do
122
+ petSchema = schema(name: 'pet_ferret')
123
+
124
+ post '/subjects/ferret/versions', { schema: petSchema }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
125
+ pet_id = JSON.parse(last_response.body).fetch('id')
126
+
127
+ get "/schemas/ids/#{pet_id}"
128
+ result = JSON.parse(last_response.body)
129
+
130
+ expect(result['schema']).to eq(petSchema)
131
+
132
+ get "/schemas/ids/#{pet_id}?subject=:.:"
133
+ default_context_result = JSON.parse(last_response.body)
134
+
135
+ expect(default_context_result['schema']).to eq(petSchema)
136
+ end
137
+
138
+ it 'returns schema by id from a non default context' do
139
+ petSchema = schema(name: 'pet_ferret_too')
140
+
141
+ post '/subjects/:.pets:ferret/versions', { schema: petSchema }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
142
+ pet_id = JSON.parse(last_response.body).fetch('id')
143
+
144
+ get "/schemas/ids/#{pet_id}?subject=:.pets:"
145
+ result = JSON.parse(last_response.body)
146
+
147
+ expect(result['schema']).to eq(petSchema)
148
+ end
149
+ end
150
+
151
+ describe 'GET /subjects' do
152
+ context 'with a clean registry' do
153
+ before do
154
+ FakeConfluentSchemaRegistryServer.clear
155
+ end
156
+
157
+ it "returns subjects from all contexts", :aggregate_failures do
158
+ post '/subjects/ferret/versions', { schema: schema(name: 'ferret') }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
159
+ post '/subjects/:.pets:cat/versions', { schema: schema(name: 'pet_cat') }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
160
+
161
+ get "/subjects"
162
+ result = JSON.parse(last_response.body)
163
+
164
+ expect(result).to include('ferret')
165
+ expect(result).to include(':.pets:cat')
166
+ end
167
+ end
168
+ end
169
+
170
+ describe 'GET /subjects/:subject/versions' do
171
+ it 'returns versions of the schema' do
172
+ post '/subjects/gerbil/versions', { schema: schema(name: 'v1') }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
173
+ post '/subjects/gerbil/versions', { schema: schema(name: 'v2') }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
174
+
175
+ get "/subjects/gerbil/versions"
176
+ result = JSON.parse(last_response.body)
177
+
178
+ expect(result).to include(1)
179
+ expect(result).to include(2)
180
+ end
181
+
182
+ it 'returns does not see versions ion another context' do
183
+ post '/subjects/gerbil/versions', { schema: schema(name: 'v1') }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
184
+ post '/subjects/:.test:gerbil/versions', { schema: schema(name: 'v2') }.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
185
+
186
+ get "/subjects/:.test:gerbil/versions"
187
+ result = JSON.parse(last_response.body)
188
+
189
+ expect(result).to include(1)
190
+ end
191
+ end
192
+
193
+ describe 'GET /subjects/:subject/versions/:version', :aggregate_failures do
194
+ it 'returns the schema by version' do
195
+ schema1 = schema(name: 'v1')
196
+ post '/subjects/gerbil/versions', { schema: schema1}.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
197
+ id1 = JSON.parse(last_response.body).fetch('id')
198
+
199
+ schema2 = schema(name: 'v2')
200
+ post '/subjects/gerbil/versions', { schema: schema2}.to_json, 'CONTENT_TYPE' => 'application/vnd.schemaregistry+json'
201
+ id2 = JSON.parse(last_response.body).fetch('id')
202
+
203
+ get '/subjects/gerbil/versions/1'
204
+ result = JSON.parse(last_response.body)
205
+ expect(result['subject']).to eq('gerbil')
206
+ expect(result['version']).to eq(1)
207
+ expect(result['id']).to eq(id1)
208
+ expect(result['schema']).to eq(schema1)
209
+
210
+ get '/subjects/gerbil/versions/2'
211
+ result = JSON.parse(last_response.body)
212
+ expect(result['subject']).to eq('gerbil')
213
+ expect(result['version']).to eq(2)
214
+ expect(result['id']).to eq(id2)
215
+ expect(result['schema']).to eq(schema2)
216
+ end
217
+ end
218
+
219
+ def schema(name:)
220
+ {
221
+ type: "record",
222
+ name: name,
223
+ fields: [
224
+ { name: "name", type: "string" },
225
+ ]
226
+ }.to_json
100
227
  end
101
228
  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: 1.14.0
4
+ version: 1.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Schierbeck
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-02 00:00:00.000000000 Z
11
+ date: 2023-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: avro
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.8.0
19
+ version: 1.11.3
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '1.12'
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 1.8.0
29
+ version: 1.11.3
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '1.12'