config_mapper 1.4.1 → 1.5.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 +84 -25
- data/lib/config_mapper/config_dict.rb +44 -5
- data/lib/config_mapper/config_struct.rb +115 -70
- data/lib/config_mapper/factory.rb +25 -0
- data/lib/config_mapper/validator.rb +16 -0
- data/lib/config_mapper/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 071bbe40a32e09004ef7e5a12ddc9eb909b7e06a
|
4
|
+
data.tar.gz: c49eff8eda1e383a3e02f72aa6c7baca0f8810c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8f8f2fdbf001b744b04c2ceb39c64c5df448568ea4b4e915d12060f0571c6bc2a8ad0581b27a545403066881f79a98f3f1633de2239959d90558967e2392089e
|
7
|
+
data.tar.gz: 51f1cffdbbccc557e8a13b7c0882b5cdaefef07d167971a79c22ffe20fc5153c37ae77c8381045c04681715514dabb8ef24e0c9b2334fef454b55a5fc3a67c89
|
data/README.md
CHANGED
@@ -1,10 +1,26 @@
|
|
1
1
|
# ConfigMapper
|
2
2
|
|
3
|
-
[](https://badge.fury.io/rb/config_mapper)
|
4
|
+
[](https://travis-ci.org/mdub/config_mapper)
|
5
5
|
|
6
6
|
ConfigMapper maps configuration data onto Ruby objects.
|
7
7
|
|
8
|
+
<!-- TOC depthFrom:2 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
|
9
|
+
|
10
|
+
- [Usage](#usage)
|
11
|
+
- [Target object](#target-object)
|
12
|
+
- [Errors](#errors)
|
13
|
+
- [ConfigStruct](#configstruct)
|
14
|
+
- [Attributes](#attributes)
|
15
|
+
- [Type validation/coercion](#type-validationcoercion)
|
16
|
+
- [Defaults](#defaults)
|
17
|
+
- [Semantic errors](#semantic-errors)
|
18
|
+
- [License](#license)
|
19
|
+
- [Contributing](#contributing)
|
20
|
+
- [See also](#see-also)
|
21
|
+
|
22
|
+
<!-- /TOC -->
|
23
|
+
|
8
24
|
## Usage
|
9
25
|
|
10
26
|
Imagine you have some Ruby objects:
|
@@ -116,34 +132,65 @@ ConfigMapper works pretty well with plain old Ruby objects, but we
|
|
116
132
|
provide a base-class, `ConfigMapper::ConfigStruct`, with a DSL that
|
117
133
|
makes it even easier to declare configuration data-structures.
|
118
134
|
|
135
|
+
### Attributes
|
136
|
+
|
137
|
+
The `attribute` method is similar to `attr_accessor`, defining both reader and writer methods for the named attribute.
|
138
|
+
|
119
139
|
```ruby
|
120
140
|
require "config_mapper/config_struct"
|
121
141
|
|
122
142
|
class State < ConfigMapper::ConfigStruct
|
123
143
|
|
124
|
-
|
125
|
-
|
126
|
-
|
144
|
+
attribute :orientation
|
145
|
+
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
### Type validation/coercion
|
150
|
+
|
151
|
+
If you specify a block when declaring an attribute, it will be invoked as part of the attribute's writer-method, to validate values when they are set. It should expect a single argument, and raise `ArgumentError` to signal invalid input. As the return value will be used as the value of the attribute, it's also an opportunity coerce values into canonical form.
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
class Server < ConfigMapper::ConfigStruct
|
155
|
+
|
156
|
+
attribute :host do |arg|
|
157
|
+
unless arg =~ /^\w+(\.\w+)+$/
|
158
|
+
raise ArgumentError, "invalid hostname: #{arg}"
|
159
|
+
end
|
160
|
+
arg
|
127
161
|
end
|
128
162
|
|
129
|
-
attribute :
|
163
|
+
attribute :port do |arg|
|
164
|
+
Integer(arg)
|
165
|
+
end
|
130
166
|
|
131
167
|
end
|
132
168
|
```
|
133
169
|
|
134
|
-
`
|
170
|
+
Alternatively, specify a "validator" as a second argument to `attribute`. It should be an object that responds to `#call`, with the same semantics described above. Good choices include `Proc` or `Method` objects, or type-objects from the [dry-types](http://dry-rb.org/gems/dry-types/) project.
|
135
171
|
|
136
172
|
```ruby
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
173
|
+
class Server < ConfigMapper::ConfigStruct
|
174
|
+
|
175
|
+
attribute :host, Types::Strict::String.constrained(format: /^\w+(\.\w+)+$/)
|
176
|
+
attribute :port, method(:Integer)
|
177
|
+
|
178
|
+
end
|
142
179
|
```
|
143
180
|
|
144
|
-
|
181
|
+
For convenience, primitive Ruby types such as `Integer` and `Float` can be used as shorthand for their namesake type-coercion methods on `Kernel`:
|
145
182
|
|
146
|
-
|
183
|
+
```ruby
|
184
|
+
class Server < ConfigMapper::ConfigStruct
|
185
|
+
|
186
|
+
attribute :port, Integer
|
187
|
+
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
### Defaults
|
192
|
+
|
193
|
+
Attributes can be given default values, e.g.
|
147
194
|
|
148
195
|
```ruby
|
149
196
|
class Address < ConfigMapper::ConfigStruct
|
@@ -153,25 +200,37 @@ class Address < ConfigMapper::ConfigStruct
|
|
153
200
|
end
|
154
201
|
```
|
155
202
|
|
156
|
-
|
203
|
+
Specify a default value of `nil` to mark an attribute as optional. Attributes without a default are treated as "required".
|
157
204
|
|
158
|
-
|
159
|
-
class Server < ConfigMapper::ConfigStruct
|
205
|
+
### Sub-components
|
160
206
|
|
161
|
-
|
162
|
-
unless arg =~ /^\w+(\.\w+)+$/
|
163
|
-
raise ArgumentError, "invalid hostname: #{arg}"
|
164
|
-
end
|
165
|
-
arg
|
166
|
-
end
|
207
|
+
The `component` method defines a nested component object, itself a `ConfigStruct`.
|
167
208
|
|
168
|
-
|
169
|
-
|
209
|
+
```ruby
|
210
|
+
class State < ConfigMapper::ConfigStruct
|
211
|
+
|
212
|
+
component :position do
|
213
|
+
attribute :x
|
214
|
+
attribute :y
|
170
215
|
end
|
171
216
|
|
172
217
|
end
|
173
218
|
```
|
174
219
|
|
220
|
+
### Semantic errors
|
221
|
+
|
222
|
+
`ConfigStruct#config_errors` returns errors for each unset mandatory attribute.
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
state = State.new
|
226
|
+
state.position.x = 3
|
227
|
+
state.position.y = 4
|
228
|
+
state.config_errors
|
229
|
+
#=> { ".orientation" => #<ConfigMapper::ConfigStruct::NoValueProvided: no value provided> }
|
230
|
+
```
|
231
|
+
|
232
|
+
`#config_errors` can be overridden to provide custom semantic validation.
|
233
|
+
|
175
234
|
`ConfigStruct#configure_with` maps data into the object, and combines mapping errors and semantic errors (returned by `#config_errors`) into a single Hash:
|
176
235
|
|
177
236
|
```ruby
|
@@ -1,18 +1,45 @@
|
|
1
|
+
require "config_mapper/factory"
|
2
|
+
require "config_mapper/validator"
|
1
3
|
require "forwardable"
|
2
4
|
|
3
5
|
module ConfigMapper
|
4
6
|
|
5
7
|
class ConfigDict
|
6
8
|
|
7
|
-
|
8
|
-
|
9
|
-
|
9
|
+
class Factory
|
10
|
+
|
11
|
+
def initialize(entry_factory, key_validator)
|
12
|
+
@entry_factory = ConfigMapper::Factory.resolve(entry_factory)
|
13
|
+
@key_validator = ConfigMapper::Validator.resolve(key_validator)
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :entry_factory
|
17
|
+
attr_reader :key_validator
|
18
|
+
|
19
|
+
def new
|
20
|
+
ConfigDict.new(@entry_factory, @key_validator)
|
21
|
+
end
|
22
|
+
|
23
|
+
def config_doc
|
24
|
+
return {} unless entry_factory.respond_to?(:config_doc)
|
25
|
+
{}.tap do |result|
|
26
|
+
entry_factory.config_doc.each do |path, doc|
|
27
|
+
result["[X]#{path}"] = doc
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(entry_factory, key_validator = nil)
|
35
|
+
@entry_factory = entry_factory
|
36
|
+
@key_validator = key_validator
|
10
37
|
@entries = {}
|
11
38
|
end
|
12
39
|
|
13
40
|
def [](key)
|
14
|
-
key = @
|
15
|
-
@entries[key] ||= @
|
41
|
+
key = @key_validator.call(key) if @key_validator
|
42
|
+
@entries[key] ||= @entry_factory.new
|
16
43
|
end
|
17
44
|
|
18
45
|
def to_h
|
@@ -23,6 +50,18 @@ module ConfigMapper
|
|
23
50
|
end
|
24
51
|
end
|
25
52
|
|
53
|
+
def config_errors
|
54
|
+
{}.tap do |errors|
|
55
|
+
each do |key, value|
|
56
|
+
prefix = "[#{key.inspect}]"
|
57
|
+
next unless value.respond_to?(:config_errors)
|
58
|
+
value.config_errors.each do |path, path_errors|
|
59
|
+
errors["#{prefix}#{path}"] = path_errors
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
26
65
|
extend Forwardable
|
27
66
|
|
28
67
|
def_delegators :@entries, :each, :empty?, :key?, :keys, :map, :size
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require "config_mapper"
|
2
2
|
require "config_mapper/config_dict"
|
3
|
+
require "config_mapper/factory"
|
4
|
+
require "config_mapper/validator"
|
3
5
|
|
4
6
|
module ConfigMapper
|
5
7
|
|
@@ -18,28 +20,31 @@ module ConfigMapper
|
|
18
20
|
# validate the argument.
|
19
21
|
#
|
20
22
|
# @param name [Symbol] attribute name
|
21
|
-
# @
|
23
|
+
# @param default default value
|
22
24
|
# @yield type-coercion block
|
23
25
|
#
|
24
|
-
def attribute(name,
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
required =
|
26
|
+
def attribute(name, type = nil, default: :no_default, description: nil, &type_block)
|
27
|
+
|
28
|
+
attribute = attribute!(name)
|
29
|
+
attribute.description = description
|
30
|
+
|
31
|
+
if default == :no_default
|
32
|
+
attribute.required = true
|
33
|
+
else
|
34
|
+
attribute.default = default.freeze
|
31
35
|
end
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
define_method("#{name}=") do |value|
|
36
|
+
|
37
|
+
attribute.validator = Validator.resolve(type || type_block)
|
38
|
+
|
39
|
+
define_method("#{attribute.name}=") do |value|
|
36
40
|
if value.nil?
|
37
|
-
raise NoValueProvided if required
|
41
|
+
raise NoValueProvided if attribute.required
|
38
42
|
else
|
39
|
-
value =
|
43
|
+
value = attribute.validator.call(value) if attribute.validator
|
40
44
|
end
|
41
|
-
instance_variable_set("@#{name}", value)
|
45
|
+
instance_variable_set("@#{attribute.name}", value)
|
42
46
|
end
|
47
|
+
|
43
48
|
end
|
44
49
|
|
45
50
|
# Defines a sub-component.
|
@@ -48,17 +53,13 @@ module ConfigMapper
|
|
48
53
|
# sub-components class.
|
49
54
|
#
|
50
55
|
# @param name [Symbol] component name
|
51
|
-
# @
|
52
|
-
# component base-class
|
56
|
+
# @param type [Class] component base-class
|
53
57
|
#
|
54
|
-
def component(name,
|
55
|
-
name = name.to_sym
|
56
|
-
declared_components << name
|
57
|
-
type = options.fetch(:type, ConfigStruct)
|
58
|
+
def component(name, type: ConfigStruct, description: nil, &block)
|
58
59
|
type = Class.new(type, &block) if block
|
59
|
-
|
60
|
-
|
61
|
-
|
60
|
+
attribute = attribute!(name)
|
61
|
+
attribute.description = description
|
62
|
+
attribute.factory = type
|
62
63
|
end
|
63
64
|
|
64
65
|
# Defines an associative array of sub-components.
|
@@ -67,53 +68,53 @@ module ConfigMapper
|
|
67
68
|
# sub-components class.
|
68
69
|
#
|
69
70
|
# @param name [Symbol] dictionary attribute name
|
70
|
-
# @
|
71
|
-
#
|
72
|
-
# @options options [String] :type (ConfigMapper::ConfigStruct)
|
73
|
-
# base-class for sub-component values
|
71
|
+
# @param type [Class] base-class for component values
|
72
|
+
# @param key_type [Proc] function used to validate keys
|
74
73
|
#
|
75
|
-
def component_dict(name,
|
76
|
-
name = name.to_sym
|
77
|
-
declared_component_dicts << name
|
78
|
-
type = options.fetch(:type, ConfigStruct)
|
74
|
+
def component_dict(name, type: ConfigStruct, key_type: nil, description: nil, &block)
|
79
75
|
type = Class.new(type, &block) if block
|
80
|
-
type
|
81
|
-
key_type = options[:key_type]
|
82
|
-
key_type = key_type.method(:new) if key_type.respond_to?(:new)
|
83
|
-
attribute_initializers[name] = lambda do
|
84
|
-
ConfigDict.new(type, key_type)
|
85
|
-
end
|
86
|
-
attr_reader name
|
76
|
+
component(name, type: ConfigDict::Factory.new(type, key_type), description: description)
|
87
77
|
end
|
88
78
|
|
89
|
-
|
90
|
-
|
79
|
+
# Generate documentation, as Ruby data.
|
80
|
+
#
|
81
|
+
# Returns an entry for each configurable path, detailing
|
82
|
+
# `description`, `type`, and `default`.
|
83
|
+
#
|
84
|
+
# @return [Hash] documentation, keyed by path
|
85
|
+
#
|
86
|
+
def config_doc
|
87
|
+
each_attribute.sort_by(&:name).map(&:config_doc).inject({}, :merge)
|
91
88
|
end
|
92
89
|
|
93
|
-
def
|
94
|
-
|
90
|
+
def attributes
|
91
|
+
attributes_by_name.values
|
95
92
|
end
|
96
93
|
|
97
|
-
def
|
98
|
-
|
94
|
+
def each_attribute(&action)
|
95
|
+
return enum_for(:each_attribute) unless action
|
96
|
+
ancestors.each do |klass|
|
97
|
+
next unless klass.respond_to?(:attributes)
|
98
|
+
klass.attributes.each(&action)
|
99
|
+
end
|
99
100
|
end
|
100
101
|
|
101
|
-
|
102
|
-
|
102
|
+
private
|
103
|
+
|
104
|
+
def attributes_by_name
|
105
|
+
@attributes_by_name ||= {}
|
103
106
|
end
|
104
107
|
|
105
|
-
def
|
106
|
-
|
107
|
-
|
108
|
-
klass.public_send(attribute).each(&action)
|
109
|
-
end
|
108
|
+
def attribute!(name)
|
109
|
+
attr_reader(name)
|
110
|
+
attributes_by_name[name] ||= Attribute.new(name)
|
110
111
|
end
|
111
112
|
|
112
113
|
end
|
113
114
|
|
114
115
|
def initialize
|
115
|
-
self.class.
|
116
|
-
instance_variable_set("@#{name}",
|
116
|
+
self.class.each_attribute do |attribute|
|
117
|
+
instance_variable_set("@#{attribute.name}", attribute.initial_value)
|
117
118
|
end
|
118
119
|
end
|
119
120
|
|
@@ -141,12 +142,12 @@ module ConfigMapper
|
|
141
142
|
#
|
142
143
|
def to_h
|
143
144
|
{}.tap do |result|
|
144
|
-
self.class.
|
145
|
-
value = send(
|
145
|
+
self.class.each_attribute do |attribute|
|
146
|
+
value = send(attribute.name)
|
146
147
|
if value && value.respond_to?(:to_h) && !value.is_a?(Array)
|
147
148
|
value = value.to_h
|
148
149
|
end
|
149
|
-
result[
|
150
|
+
result[attribute.name.to_s] = value
|
150
151
|
end
|
151
152
|
end
|
152
153
|
end
|
@@ -155,13 +156,9 @@ module ConfigMapper
|
|
155
156
|
|
156
157
|
def components
|
157
158
|
{}.tap do |result|
|
158
|
-
self.class.
|
159
|
-
|
160
|
-
|
161
|
-
self.class.for_all(:declared_component_dicts) do |name|
|
162
|
-
instance_variable_get("@#{name}").each do |key, value|
|
163
|
-
result[".#{name}[#{key.inspect}]"] = value
|
164
|
-
end
|
159
|
+
self.class.each_attribute do |a|
|
160
|
+
next unless a.factory
|
161
|
+
result[".#{a.name}"] = instance_variable_get("@#{a.name}")
|
165
162
|
end
|
166
163
|
end
|
167
164
|
end
|
@@ -176,9 +173,9 @@ module ConfigMapper
|
|
176
173
|
|
177
174
|
def missing_required_attribute_errors
|
178
175
|
{}.tap do |errors|
|
179
|
-
self.class.
|
180
|
-
if instance_variable_get("@#{name}").nil?
|
181
|
-
errors[".#{name}"] = NoValueProvided.new
|
176
|
+
self.class.each_attribute do |a|
|
177
|
+
if a.required && instance_variable_get("@#{a.name}").nil?
|
178
|
+
errors[".#{a.name}"] = NoValueProvided.new
|
182
179
|
end
|
183
180
|
end
|
184
181
|
end
|
@@ -186,13 +183,61 @@ module ConfigMapper
|
|
186
183
|
|
187
184
|
def component_config_errors
|
188
185
|
{}.tap do |errors|
|
189
|
-
components.each do |
|
186
|
+
components.each do |component_path, component_value|
|
190
187
|
next unless component_value.respond_to?(:config_errors)
|
191
|
-
component_value.config_errors.each do |
|
192
|
-
errors["#{
|
188
|
+
component_value.config_errors.each do |path, value|
|
189
|
+
errors["#{component_path}#{path}"] = value
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
class Attribute
|
196
|
+
|
197
|
+
def initialize(name)
|
198
|
+
@name = name.to_sym
|
199
|
+
end
|
200
|
+
|
201
|
+
attr_reader :name
|
202
|
+
|
203
|
+
attr_accessor :description
|
204
|
+
attr_accessor :factory
|
205
|
+
attr_accessor :validator
|
206
|
+
attr_accessor :default
|
207
|
+
attr_accessor :required
|
208
|
+
|
209
|
+
def initial_value
|
210
|
+
return factory.new if factory
|
211
|
+
default
|
212
|
+
end
|
213
|
+
|
214
|
+
def factory=(arg)
|
215
|
+
@factory = Factory.resolve(arg)
|
216
|
+
end
|
217
|
+
|
218
|
+
def config_doc
|
219
|
+
self_doc.merge(type_doc)
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
def self_doc
|
225
|
+
{
|
226
|
+
".#{name}" => {}.tap do |doc|
|
227
|
+
doc["description"] = description if description
|
228
|
+
doc["default"] = default if default
|
229
|
+
doc["type"] = String(validator.name) if validator.respond_to?(:name)
|
193
230
|
end
|
231
|
+
}
|
232
|
+
end
|
233
|
+
|
234
|
+
def type_doc
|
235
|
+
return {} unless factory.respond_to?(:config_doc)
|
236
|
+
factory.config_doc.each_with_object({}) do |(path, doc), result|
|
237
|
+
result[".#{name}#{path}"] = doc
|
194
238
|
end
|
195
239
|
end
|
240
|
+
|
196
241
|
end
|
197
242
|
|
198
243
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ConfigMapper
|
2
|
+
|
3
|
+
module Factory
|
4
|
+
|
5
|
+
def self.resolve(arg)
|
6
|
+
return arg if arg.respond_to?(:new)
|
7
|
+
return ProcFactory.new(arg) if arg.respond_to?(:call)
|
8
|
+
raise ArgumentError, "invalid factory"
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class ProcFactory
|
14
|
+
|
15
|
+
def initialize(f)
|
16
|
+
@f = f
|
17
|
+
end
|
18
|
+
|
19
|
+
def new
|
20
|
+
@f.call
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ConfigMapper
|
2
|
+
|
3
|
+
module Validator
|
4
|
+
|
5
|
+
def self.resolve(arg)
|
6
|
+
return arg if arg.respond_to?(:call)
|
7
|
+
if arg.respond_to?(:name)
|
8
|
+
# looks like a primitive class -- find the corresponding coercion method
|
9
|
+
return Kernel.method(arg.name)
|
10
|
+
end
|
11
|
+
arg
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: config_mapper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-08-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -65,8 +65,10 @@ files:
|
|
65
65
|
- lib/config_mapper/collection_mapper.rb
|
66
66
|
- lib/config_mapper/config_dict.rb
|
67
67
|
- lib/config_mapper/config_struct.rb
|
68
|
+
- lib/config_mapper/factory.rb
|
68
69
|
- lib/config_mapper/mapper.rb
|
69
70
|
- lib/config_mapper/object_mapper.rb
|
71
|
+
- lib/config_mapper/validator.rb
|
70
72
|
- lib/config_mapper/version.rb
|
71
73
|
homepage: https://github.com/mdub/config_mapper
|
72
74
|
licenses:
|
@@ -80,7 +82,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
82
|
requirements:
|
81
83
|
- - ">="
|
82
84
|
- !ruby/object:Gem::Version
|
83
|
-
version: '0'
|
85
|
+
version: '2.0'
|
84
86
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
87
|
requirements:
|
86
88
|
- - ">="
|