config_mapper 1.4.1 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Gem Version](https://badge.fury.io/rb/config_mapper.
|
4
|
-
[![Build Status](https://
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/config_mapper.svg)](https://badge.fury.io/rb/config_mapper)
|
4
|
+
[![Build Status](https://travis-ci.org/mdub/config_mapper.svg?branch=master)](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
|
- - ">="
|