avro_turf 1.14.0 → 1.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/CHANGELOG.md +10 -0
- data/README.md +9 -1
- data/avro_turf.gemspec +1 -1
- data/lib/avro_turf/confluent_schema_registry.rb +13 -10
- data/lib/avro_turf/messaging.rb +3 -0
- data/lib/avro_turf/schema_store.rb +1 -1
- data/lib/avro_turf/test/fake_confluent_schema_registry_server.rb +54 -24
- data/lib/avro_turf/test/fake_prefixed_confluent_schema_registry_server.rb +14 -12
- data/lib/avro_turf/version.rb +1 -1
- data/spec/confluent_schema_registry_spec.rb +47 -27
- data/spec/schema_store_spec.rb +29 -0
- data/spec/spec_helper.rb +4 -1
- data/spec/support/confluent_schema_registry_context.rb +16 -9
- data/spec/test/fake_confluent_schema_registry_server_spec.rb +160 -33
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2345cdad9c7472c5fd79aa3710150c40095ff2b970b28ea17b3db8b951fd143f
|
4
|
+
data.tar.gz: 264fbec47c582106134b51fc80823f93a82c9f1053a2d61e8899eac72a27256f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f17112a66c063514d56c7a4a5d785fa61e1c2079b7a02eede619b1b030997ba689b897ac8fc55f355fc781a1fd48dd77c545315478155b70853a0263fd571ac
|
7
|
+
data.tar.gz: 9d34639b83ee204e15143cc073aacd24157556eaede70e855384ba43ec769c6ed12505ffa2cf6d17ae6e216095c0ad097ace8af42579efaec4fc34ad92c482fe
|
data/.github/workflows/ruby.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## v1.16.0
|
6
|
+
|
7
|
+
- Add compatibility with Avro v1.12.x.
|
8
|
+
|
9
|
+
## v1.15.0
|
10
|
+
|
11
|
+
- Use `default_namespace` from exception to load nested schemas from the correct namespace. (#203)
|
12
|
+
- Bump minimum avro version to 1.11.3
|
13
|
+
- Add support for schema contexts (#205)
|
14
|
+
|
5
15
|
## v1.14.0
|
6
16
|
|
7
17
|
- 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.
|
@@ -171,11 +175,15 @@ avro = AvroTurf::Messaging.new(registry_url: "http://my-registry:8081/")
|
|
171
175
|
data = avro.encode({ "title" => "hello, world" }, schema_name: "greeting")
|
172
176
|
|
173
177
|
# If you don't want to automatically register new schemas, you can pass explicitly
|
174
|
-
# subject and version to specify which schema should be used for encoding.
|
178
|
+
# both subject and version to specify which schema should be used for encoding.
|
175
179
|
# It will fetch that schema from the registry and cache it. Subsequent instances
|
176
180
|
# of the same schema version will be served by the cache.
|
177
181
|
data = avro.encode({ "title" => "hello, world" }, subject: 'greeting', version: 1)
|
178
182
|
|
183
|
+
# If you want to use a specific local schema, but register it with a different name in the
|
184
|
+
# registry, then provide a subject and a schema_name, but not a version
|
185
|
+
data = avro.encode({ "title" => "hello, world" }, subject: "greeting-value", schema_name: "greeting")
|
186
|
+
|
179
187
|
# You can also pass explicitly schema_id to specify which schema
|
180
188
|
# should be used for encoding.
|
181
189
|
# It will fetch that schema from the registry and cache it. Subsequent instances
|
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.
|
22
|
+
spec.add_dependency "avro", ">= 1.11.3", "< 1.13"
|
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
|
data/lib/avro_turf/messaging.rb
CHANGED
@@ -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
|
-
|
5
|
-
|
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/:
|
41
|
+
post "/subjects/:qualified_subject/versions" do
|
37
42
|
schema = parse_schema
|
38
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
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
|
-
|
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.
|
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/:
|
78
|
-
|
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/:
|
84
|
-
|
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
|
-
|
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/:
|
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
|
-
|
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[:
|
129
|
+
subject: params[:qualified_subject],
|
115
130
|
id: schema_id,
|
116
|
-
version: SUBJECTS[
|
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
|
data/lib/avro_turf/version.rb
CHANGED
@@ -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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
data/spec/schema_store_spec.rb
CHANGED
@@ -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.
|
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(
|
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
|
-
|
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' =>
|
89
|
+
'subject' => expected_subject_name,
|
87
90
|
'version' => 1
|
88
91
|
])
|
89
92
|
expect(registry.schema_subject_versions(schema_id2)).to include({
|
90
|
-
'subject' =>
|
93
|
+
'subject' => expected_subject_name,
|
91
94
|
'version' => 2
|
92
95
|
},{
|
93
|
-
'subject' =>
|
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
|
-
|
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
|
-
|
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:
|
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
|
-
|
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.
|
4
|
+
version: 1.16.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:
|
11
|
+
date: 2024-08-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: avro
|
@@ -16,20 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 1.
|
19
|
+
version: 1.11.3
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '1.
|
22
|
+
version: '1.13'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 1.
|
29
|
+
version: 1.11.3
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '1.
|
32
|
+
version: '1.13'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: excon
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|