reynard 0.6.0 → 0.8.0
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 +4 -4
- data/README.md +66 -0
- data/lib/reynard/context.rb +7 -4
- data/lib/reynard/external.rb +6 -2
- data/lib/reynard/http/request.rb +5 -1
- data/lib/reynard/http/response.rb +5 -3
- data/lib/reynard/http.rb +1 -0
- data/lib/reynard/inflector.rb +30 -0
- data/lib/reynard/model.rb +29 -5
- data/lib/reynard/object_builder.rb +9 -5
- data/lib/reynard/specification.rb +4 -3
- data/lib/reynard/version.rb +1 -1
- data/lib/reynard.rb +11 -2
- metadata +4 -59
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96421332b89c0816b77704aba7b07f267424c0a238fd5b74c8b91aa9d3943c6d
|
4
|
+
data.tar.gz: a6a714f984bd420fe2254b747972ba804968cc4df6ca534b38c50c1cf2b2687b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7cbb02d762e3c38b05b59ec25f8320f3a5830b56aabe376249f2347a098b43cff31935829d9dd831edc383a62531a06b78e735d886bf4f2329475e8785820211
|
7
|
+
data.tar.gz: 767b9ba9f0f5eb3fae98d414f778e481fd6b3c0a928128cec29b3a409db44481057afc44c9ac029a98892b71c3d3e09fd80286e0b2e942e47aef97b2921fab22
|
data/README.md
CHANGED
@@ -152,6 +152,72 @@ For example, in case of an array item it would look at `books` and singularize i
|
|
152
152
|
|
153
153
|
If you run into issues where Reynard doesn't properly build an object for a nested resource, it's probably because of a naming issue. It's advised to add a `title` property to the schema definition with a unique name in that case.
|
154
154
|
|
155
|
+
### Properties and model attributes
|
156
|
+
|
157
|
+
Reynard provides access to JSON properties on the model in a number of ways. There are some restrictions because of Ruby, so it's good to understand them.
|
158
|
+
|
159
|
+
Let's assume there is a payload for an `Author` model that looks like this:
|
160
|
+
|
161
|
+
```json
|
162
|
+
{"first_name":"Marcél","lastName":"Marcellus","1st-class":false}
|
163
|
+
```
|
164
|
+
|
165
|
+
Reynard attemps to give access to these properties as much as possible by sanitizing and normalizing them, so you can do the following:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
response.object.first_name #=> "Marcél"
|
169
|
+
response.object.last_name #=> "Marcellus"
|
170
|
+
```
|
171
|
+
|
172
|
+
But it's also possible to use the original casing for `lastName`.
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
response.object.lastName #=> "Marcellus"
|
176
|
+
```
|
177
|
+
|
178
|
+
However, a method can't start with a number and can't contain dashes in Ruby so the following is not possible:
|
179
|
+
|
180
|
+
```
|
181
|
+
# Not valid Ruby syntax:
|
182
|
+
response.object.1st-class
|
183
|
+
```
|
184
|
+
|
185
|
+
There are two alternatives for accessing this property:
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
# The preferred solution for accessing raw property values is through the
|
189
|
+
# parsed JSON on the response object.
|
190
|
+
response.parsed_body["1st-class"]
|
191
|
+
# When you are processing nested models and you don't have access to the
|
192
|
+
# response object, you can chose to use the `[]` method.
|
193
|
+
response.object["1st-class"]
|
194
|
+
# Don't use `send` to access the property, this may not work in future
|
195
|
+
# versions.
|
196
|
+
response.object.send("1st-class")
|
197
|
+
```
|
198
|
+
|
199
|
+
#### Mapping properties
|
200
|
+
|
201
|
+
In case you are forced to access a property through a method, you could chose to map irregular property names to method names globally for all models:
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
reynard.snake_cases({ "1st-class" => "first_class" })
|
205
|
+
```
|
206
|
+
|
207
|
+
This will allow you to access the property through the `first_class` method without changing the behavior of the rest of the object.
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
response.object.first_class #=> false
|
211
|
+
response.object["1st-class"] #=> false
|
212
|
+
```
|
213
|
+
|
214
|
+
Don't use this to map common property names that would work fine otherwise, because you could make things really confusing.
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
# Don't do this.
|
218
|
+
reynard.snake_cases({ "name" => "naem" })
|
219
|
+
```
|
220
|
+
|
155
221
|
## Logging
|
156
222
|
|
157
223
|
When you want to know what the Reynard client is doing you can enable logging.
|
data/lib/reynard/context.rb
CHANGED
@@ -8,13 +8,14 @@ class Reynard
|
|
8
8
|
extend Forwardable
|
9
9
|
def_delegators :@request_context, :verb, :path, :full_path, :url
|
10
10
|
|
11
|
-
def initialize(specification:, request_context: nil)
|
11
|
+
def initialize(specification:, inflector:, request_context: nil)
|
12
12
|
@specification = specification
|
13
|
+
@inflector = inflector
|
13
14
|
@request_context = request_context || build_request_context
|
14
15
|
end
|
15
16
|
|
16
17
|
def base_url(base_url)
|
17
|
-
copy(base_url:)
|
18
|
+
copy(base_url: base_url)
|
18
19
|
end
|
19
20
|
|
20
21
|
def operation(operation_name)
|
@@ -43,7 +44,7 @@ class Reynard
|
|
43
44
|
end
|
44
45
|
|
45
46
|
def logger(logger)
|
46
|
-
copy(logger:)
|
47
|
+
copy(logger: logger)
|
47
48
|
end
|
48
49
|
|
49
50
|
def execute
|
@@ -59,6 +60,7 @@ class Reynard
|
|
59
60
|
def copy(**properties)
|
60
61
|
self.class.new(
|
61
62
|
specification: @specification,
|
63
|
+
inflector: @inflector,
|
62
64
|
request_context: @request_context.copy(**properties)
|
63
65
|
)
|
64
66
|
end
|
@@ -70,8 +72,9 @@ class Reynard
|
|
70
72
|
def build_response(http_response)
|
71
73
|
Reynard::Http::Response.new(
|
72
74
|
specification: @specification,
|
75
|
+
inflector: @inflector,
|
73
76
|
request_context: @request_context,
|
74
|
-
http_response:
|
77
|
+
http_response: http_response
|
75
78
|
)
|
76
79
|
end
|
77
80
|
end
|
data/lib/reynard/external.rb
CHANGED
@@ -8,6 +8,7 @@ class Reynard
|
|
8
8
|
def initialize(path:, ref:)
|
9
9
|
@path = path
|
10
10
|
@relative_path, @anchor = ref.split('#', 2)
|
11
|
+
@filename = File.expand_path(@relative_path, @path)
|
11
12
|
end
|
12
13
|
|
13
14
|
def path
|
@@ -22,11 +23,14 @@ class Reynard
|
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
26
|
+
def filesystem_path
|
27
|
+
File.dirname(@filename)
|
28
|
+
end
|
29
|
+
|
25
30
|
private
|
26
31
|
|
27
32
|
def filename
|
28
|
-
filename
|
29
|
-
return filename if filename.start_with?(@path)
|
33
|
+
return @filename if @filename.start_with?(@path)
|
30
34
|
|
31
35
|
raise 'You are only allowed to reference files relative to the specification file.'
|
32
36
|
end
|
data/lib/reynard/http/request.rb
CHANGED
@@ -25,8 +25,12 @@ class Reynard
|
|
25
25
|
Net::HTTP.const_get(@request_context.verb.capitalize)
|
26
26
|
end
|
27
27
|
|
28
|
+
def request_headers
|
29
|
+
{ 'User-Agent' => Reynard.user_agent }.merge(@request_context.headers || {})
|
30
|
+
end
|
31
|
+
|
28
32
|
def build_request
|
29
|
-
request = request_class.new(uri,
|
33
|
+
request = request_class.new(uri, request_headers)
|
30
34
|
if @request_context.body
|
31
35
|
@request_context.logger&.debug { @request_context.body }
|
32
36
|
request.body = @request_context.body
|
@@ -8,8 +8,9 @@ class Reynard
|
|
8
8
|
extend Forwardable
|
9
9
|
def_delegators :@http_response, :code, :content_type, :[], :body
|
10
10
|
|
11
|
-
def initialize(specification:, request_context:, http_response:)
|
11
|
+
def initialize(specification:, inflector:, request_context:, http_response:)
|
12
12
|
@specification = specification
|
13
|
+
@inflector = inflector
|
13
14
|
@request_context = request_context
|
14
15
|
@http_response = http_response
|
15
16
|
end
|
@@ -69,9 +70,10 @@ class Reynard
|
|
69
70
|
end
|
70
71
|
|
71
72
|
def build_object_with_media_type(media_type)
|
72
|
-
|
73
|
+
ObjectBuilder.new(
|
73
74
|
schema: @specification.schema(media_type.node),
|
74
|
-
|
75
|
+
inflector: @inflector,
|
76
|
+
parsed_body: parsed_body
|
75
77
|
).call
|
76
78
|
end
|
77
79
|
|
data/lib/reynard/http.rb
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Reynard
|
4
|
+
# Transforms property names so they are value Ruby identifiers or more readable to users.
|
5
|
+
class Inflector
|
6
|
+
def initialize
|
7
|
+
@snake_case = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
# Registers additional exceptions to the regular snake-case algorithm. Registering is additive
|
11
|
+
# so you can call this multiple times without losing previously registered exceptions.
|
12
|
+
def snake_cases(exceptions)
|
13
|
+
@snake_case.merge!(exceptions)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the string in snake-case, taking previously registered exceptions into account.
|
17
|
+
def snake_case(property)
|
18
|
+
@snake_case[property] || self.class.snake_case(property)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the string in snake-case.
|
22
|
+
def self.snake_case(property)
|
23
|
+
property
|
24
|
+
.to_s
|
25
|
+
.gsub(/([A-Z])(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { (Regexp.last_match(1) || Regexp.last_match(2)) << '_' }
|
26
|
+
.tr("'\"-", '___')
|
27
|
+
.downcase
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/reynard/model.rb
CHANGED
@@ -3,30 +3,41 @@
|
|
3
3
|
class Reynard
|
4
4
|
# Superclass for dynamic classes generated by the object builder.
|
5
5
|
class Model
|
6
|
+
extend Forwardable
|
7
|
+
def_delegators :@attributes, :[]
|
8
|
+
|
6
9
|
class << self
|
7
10
|
# Holds references to the full schema for the model if available.
|
8
11
|
attr_accessor :schema
|
12
|
+
# The inflector to use on properties.
|
13
|
+
attr_writer :inflector
|
9
14
|
end
|
10
15
|
|
11
16
|
def initialize(attributes)
|
17
|
+
@attributes = {}
|
18
|
+
@snake_cases = self.class.snake_cases(attributes.keys)
|
12
19
|
self.attributes = attributes
|
13
20
|
end
|
14
21
|
|
15
22
|
def attributes=(attributes)
|
16
23
|
attributes.each do |name, value|
|
17
|
-
|
24
|
+
@attributes[name.to_s] = self.class.cast(name, value)
|
18
25
|
end
|
19
26
|
end
|
20
27
|
|
21
28
|
# Until we can set accessors based on the schema
|
22
29
|
def method_missing(attribute_name, *)
|
23
|
-
|
24
|
-
|
30
|
+
attribute_name = attribute_name.to_s
|
31
|
+
@attributes[attribute_name] || @attributes.fetch(@snake_cases.fetch(attribute_name))
|
32
|
+
rescue KeyError
|
25
33
|
raise NoMethodError, "undefined method `#{attribute_name}' for #{inspect}"
|
26
34
|
end
|
27
35
|
|
28
36
|
def respond_to_missing?(attribute_name, *)
|
29
|
-
|
37
|
+
attribute_name = attribute_name.to_s
|
38
|
+
return true if @attributes.key?(attribute_name)
|
39
|
+
|
40
|
+
@snake_cases.key?(attribute_name) && @attributes.key?(@snake_cases[attribute_name])
|
30
41
|
rescue NameError
|
31
42
|
false
|
32
43
|
end
|
@@ -37,7 +48,20 @@ class Reynard
|
|
37
48
|
property = schema.property_schema(name)
|
38
49
|
return value unless property
|
39
50
|
|
40
|
-
Reynard::ObjectBuilder.new(schema: property, parsed_body: value).call
|
51
|
+
Reynard::ObjectBuilder.new(schema: property, inflector: inflector, parsed_body: value).call
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.inflector
|
55
|
+
@inflector ||= Inflector.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.snake_cases(property_names)
|
59
|
+
property_names.each_with_object({}) do |property_name, snake_cases|
|
60
|
+
snake_case = inflector.snake_case(property_name)
|
61
|
+
next if snake_case == property_name
|
62
|
+
|
63
|
+
snake_cases[snake_case] = property_name
|
64
|
+
end
|
41
65
|
end
|
42
66
|
end
|
43
67
|
end
|
@@ -7,8 +7,9 @@ class Reynard
|
|
7
7
|
class ObjectBuilder
|
8
8
|
attr_reader :schema, :parsed_body
|
9
9
|
|
10
|
-
def initialize(schema:, parsed_body:, model_name: nil)
|
10
|
+
def initialize(schema:, inflector:, parsed_body:, model_name: nil)
|
11
11
|
@schema = schema
|
12
|
+
@inflector = inflector
|
12
13
|
@parsed_body = parsed_body
|
13
14
|
@model_name = model_name
|
14
15
|
end
|
@@ -21,7 +22,8 @@ class Reynard
|
|
21
22
|
return @model_class if defined?(@model_class)
|
22
23
|
|
23
24
|
@model_class =
|
24
|
-
self.class.model_class_get(model_name) ||
|
25
|
+
self.class.model_class_get(model_name) ||
|
26
|
+
self.class.model_class_set(model_name, schema, @inflector)
|
25
27
|
end
|
26
28
|
|
27
29
|
def call
|
@@ -41,11 +43,11 @@ class Reynard
|
|
41
43
|
nil
|
42
44
|
end
|
43
45
|
|
44
|
-
def self.model_class_set(model_name, schema)
|
46
|
+
def self.model_class_set(model_name, schema, inflector)
|
45
47
|
if schema.type == 'array'
|
46
48
|
array_model_class_set(model_name)
|
47
49
|
else
|
48
|
-
object_model_class_set(model_name, schema)
|
50
|
+
object_model_class_set(model_name, schema, inflector)
|
49
51
|
end
|
50
52
|
end
|
51
53
|
|
@@ -55,11 +57,12 @@ class Reynard
|
|
55
57
|
::Reynard::Models.const_set(model_name, Class.new(Array))
|
56
58
|
end
|
57
59
|
|
58
|
-
def self.object_model_class_set(model_name, schema)
|
60
|
+
def self.object_model_class_set(model_name, schema, inflector)
|
59
61
|
return Reynard::Model unless model_name
|
60
62
|
|
61
63
|
model_class = Class.new(Reynard::Model)
|
62
64
|
model_class.schema = schema
|
65
|
+
model_class.inflector = inflector
|
63
66
|
::Reynard::Models.const_set(model_name, model_class)
|
64
67
|
end
|
65
68
|
|
@@ -73,6 +76,7 @@ class Reynard
|
|
73
76
|
parsed_body.each do |item|
|
74
77
|
array << self.class.new(
|
75
78
|
schema: item_schema,
|
79
|
+
inflector: @inflector,
|
76
80
|
parsed_body: item
|
77
81
|
).call
|
78
82
|
end
|
@@ -14,7 +14,7 @@ class Reynard
|
|
14
14
|
|
15
15
|
# Digs a value out of the specification, taking $ref into account.
|
16
16
|
def dig(*path)
|
17
|
-
dig_into(@data, @data, path.dup)
|
17
|
+
dig_into(@data, @data, path.dup, File.dirname(@filename))
|
18
18
|
end
|
19
19
|
|
20
20
|
def servers
|
@@ -103,7 +103,7 @@ class Reynard
|
|
103
103
|
# rubocop:disable Metrics/AbcSize
|
104
104
|
# rubocop:disable Metrics/CyclomaticComplexity
|
105
105
|
# rubocop:disable Metrics/MethodLength
|
106
|
-
def dig_into(data, cursor, path)
|
106
|
+
def dig_into(data, cursor, path, filesystem_path)
|
107
107
|
while path.length.positive?
|
108
108
|
cursor = cursor[path.first]
|
109
109
|
return unless cursor
|
@@ -118,7 +118,8 @@ class Reynard
|
|
118
118
|
cursor = data
|
119
119
|
# References another file, with an optional anchor to an element in the data.
|
120
120
|
when %r{\A\./}
|
121
|
-
external = External.new(path:
|
121
|
+
external = External.new(path: filesystem_path, ref: cursor['$ref'])
|
122
|
+
filesystem_path = external.filesystem_path
|
122
123
|
path = external.path + path
|
123
124
|
cursor = external.data
|
124
125
|
end
|
data/lib/reynard/version.rb
CHANGED
data/lib/reynard.rb
CHANGED
@@ -13,11 +13,13 @@ class Reynard
|
|
13
13
|
extend Forwardable
|
14
14
|
def_delegators :build_context, :logger, :base_url, :operation, :headers, :params
|
15
15
|
def_delegators :@specification, :servers
|
16
|
+
def_delegators :@inflector, :snake_cases
|
16
17
|
|
17
18
|
autoload :Context, 'reynard/context'
|
18
19
|
autoload :External, 'reynard/external'
|
19
20
|
autoload :GroupedParameters, 'reynard/grouped_parameters'
|
20
21
|
autoload :Http, 'reynard/http'
|
22
|
+
autoload :Inflector, 'reynard/inflector'
|
21
23
|
autoload :Logger, 'reynard/logger'
|
22
24
|
autoload :MediaType, 'reynard/media_type'
|
23
25
|
autoload :Model, 'reynard/model'
|
@@ -33,7 +35,8 @@ class Reynard
|
|
33
35
|
autoload :VERSION, 'reynard/version'
|
34
36
|
|
35
37
|
def initialize(filename:)
|
36
|
-
@specification = Specification.new(filename:)
|
38
|
+
@specification = Specification.new(filename: filename)
|
39
|
+
@inflector = Inflector.new
|
37
40
|
end
|
38
41
|
|
39
42
|
class << self
|
@@ -42,6 +45,12 @@ class Reynard
|
|
42
45
|
attr_writer :http
|
43
46
|
end
|
44
47
|
|
48
|
+
# Returns a value that will be used by default for Reynard's User-Agent headers. Please use
|
49
|
+
# the +headers+ setter on the context if you want to change this.
|
50
|
+
def self.user_agent
|
51
|
+
"Reynard/#{Reynard::VERSION}"
|
52
|
+
end
|
53
|
+
|
45
54
|
# Returns Reynard's global request interface. This is a global object to allow persistent
|
46
55
|
# connections, caching, and other features that need a persistent object in the process.
|
47
56
|
def self.http
|
@@ -55,6 +64,6 @@ class Reynard
|
|
55
64
|
private
|
56
65
|
|
57
66
|
def build_context
|
58
|
-
Context.new(specification: @specification)
|
67
|
+
Context.new(specification: @specification, inflector: @inflector)
|
59
68
|
end
|
60
69
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: reynard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Manfred Stienstra
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: multi_json
|
@@ -52,62 +52,6 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: minitest
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: rake
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: webmock
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: webrick
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
111
55
|
description: |2
|
112
56
|
Reynard is an OpenAPI client for Ruby. It operates directly on the OpenAPI specification without
|
113
57
|
the need to generate any source code.
|
@@ -126,6 +70,7 @@ files:
|
|
126
70
|
- lib/reynard/http.rb
|
127
71
|
- lib/reynard/http/request.rb
|
128
72
|
- lib/reynard/http/response.rb
|
73
|
+
- lib/reynard/inflector.rb
|
129
74
|
- lib/reynard/media_type.rb
|
130
75
|
- lib/reynard/model.rb
|
131
76
|
- lib/reynard/models.rb
|
@@ -151,7 +96,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
151
96
|
requirements:
|
152
97
|
- - ">"
|
153
98
|
- !ruby/object:Gem::Version
|
154
|
-
version: '3.
|
99
|
+
version: '3.0'
|
155
100
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
101
|
requirements:
|
157
102
|
- - ">="
|