attributor 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 102708c30b51b0ddb7a38488ca440ada38c2f3a8
4
- data.tar.gz: 4e50cb2a981f4b1d6173e754789a25932153d7bd
3
+ metadata.gz: 93e65f034891a43eb2164a43a0bd5bdf7d2a0e22
4
+ data.tar.gz: dc626f0b5d2cb575f490a1e1d8af6c60d232830f
5
5
  SHA512:
6
- metadata.gz: 47095d5c279287a05b302867f1e9d5518d511405d1a475e1ebd272f584c3f71284bb8d59eaab9da7ad79043a0d1179ff29514052de69a0ac48fecc9315ad35a4
7
- data.tar.gz: 495d613202a0b19dd186cd302e5fdffdaeea8e40c059e4c7ba6b7b81a175e10bd06fbf6a8b1349c9fe12c88c93c8da5beca53cf8a7e25151eb9eecd4da5a3b17
6
+ metadata.gz: 514f798b6bcb84313fa6c840dcb0a4bf9533cb3ec7f29a1c9a8ca00db45d119e7122a2bea02289fe094a6ab8db41ff13af0bfb47fafe9568186243f44d25b3f6
7
+ data.tar.gz: 848e54a8cdf56ab1a498ea75fbddc3571c7723a1285cc8b8c1085a2fa55e6b0a75494968c83f8154bab58f04531a684ea05114970e83c45b53a7c10e6e6a2883
data/.gitignore CHANGED
@@ -13,3 +13,5 @@ doc
13
13
 
14
14
  # For MacOS:
15
15
  .DS_Store
16
+
17
+ Gemfile.lock
data/CHANGELOG.md CHANGED
@@ -4,7 +4,18 @@ Attributor Changelog
4
4
  next
5
5
  ------
6
6
 
7
- * First new feature here
7
+ * next thing here
8
+
9
+ 2.2.0
10
+ ------
11
+
12
+ * Fix example generation for Hash and Collection to handle a non-Array context parameter.
13
+ * Hash:
14
+ * Added additional options:
15
+ * `:case_insensitive_load` for string-keyed hashes. This allows loading hashes with keys that do not exactly match the case defined in the hash.
16
+ * Added `:allow_extras` option to allow handling of undefined keys when loading.
17
+ * Added `Hash#set` to encapsulate the above options and attribute loading.
18
+ * Added `extra` command in the `keys` DSL, which lets you define a key (whose value should be a Hash), to group any unspecified keys during load.
8
19
 
9
20
  2.1.0
10
21
  ------
@@ -23,7 +34,7 @@ next
23
34
  * Enhanced error messages to report the correct context scope.
24
35
  * Make Attribute assignments in models to report a special context (not the attributor root)
25
36
  * Instead of reporting "$." as the context , when doing model.field_name=value, they'll now report "assignment.of(field_name)" instead
26
- * Truncate the lenght of values when reporting loading errors when they're long (i.e. >500 chars)
37
+ * Truncate the length of values when reporting loading errors when they're long (i.e. >500 chars)
27
38
  * `Model.attributes` may now be called more than once to set add or replace attributes. The exact behavior depends upon the types of the attributes being added or replaced. See [model_spec.rb](spec/types/model_spec.rb) for examples.
28
39
  * Greately enhanced Hash type with individual key specification (rather than
29
40
  simply defining the types of keys)
data/README.md CHANGED
@@ -1,4 +1,7 @@
1
- # Attributor
1
+ # Attributor [![TravisCI][travis-img-url]][travis-ci-url]
2
+
3
+ [travis-img-url]: https://travis-ci.org/rightscale/attributor.svg?branch=master
4
+ [travis-ci-url]:https://travis-ci.org/rightscale/attributor
2
5
 
3
6
  An Attribute management, self documenting framework, designed for getting rid of most of your parameter handling boilerplate.
4
7
  While initially designed to be the backbone for parameter handling in REST services, attribute management can be applied in many other areas.
data/Rakefile CHANGED
@@ -1,20 +1,12 @@
1
1
  # encoding: utf-8
2
-
3
-
4
2
  require 'bundler/setup'
5
- #begin
6
- # Bundler.setup(:default, :development)
7
- #rescue Bundler::BundlerError => e
8
- # $stderr.puts e.message
9
- # $stderr.puts "Run `bundle install` to install missing gems"
10
- # exit e.status_code
11
- #end
12
3
  require 'rake'
13
4
 
14
- require 'rake/notes/rake_task'
15
-
16
5
  require 'rspec/core'
17
6
  require 'rspec/core/rake_task'
7
+ require 'bundler/gem_tasks'
8
+ require 'rake/notes/rake_task'
9
+
18
10
 
19
11
  desc "Run RSpec code examples with simplecov"
20
12
  RSpec::Core::RakeTask.new do |spec|
@@ -70,7 +70,7 @@ module Attributor
70
70
 
71
71
 
72
72
  TOP_LEVEL_OPTIONS = [ :description, :values, :default, :example, :required, :required_if ]
73
- INTERNAL_OPTIONS = [:dsl_compiler] # Options we don't want to expose when describing attributes
73
+ INTERNAL_OPTIONS = [:dsl_compiler,:dsl_compiler_options] # Options we don't want to expose when describing attributes
74
74
  def describe(shallow=true)
75
75
  description = { }
76
76
  # Clone the common options
@@ -45,6 +45,17 @@ module Attributor
45
45
  target.keys[name] = define(name, attr_type, **opts, &block)
46
46
  end
47
47
 
48
+ def extra(name, attr_type=nil, **opts, &block)
49
+ if attr_type.nil?
50
+ attr_type = Attributor::Hash.of(key: target.key_type, value: target.value_type)
51
+ end
52
+ target.extra_keys = name
53
+ target.options[:allow_extra] = true
54
+ opts[:default] ||= {}
55
+ attr_type.options[:allow_extra] = true
56
+ key(name, attr_type, **opts, &block)
57
+ end
58
+
48
59
  # Creates an Attributor:Attribute with given definition.
49
60
  #
50
61
  # @overload define(name, type, opts, &block)
@@ -1,10 +1,3 @@
1
- # Will need eventually, but not right now:
2
- # Hash
3
- # Array
4
- # CSV
5
- # Ids
6
-
7
-
8
1
  module Attributor
9
2
 
10
3
  # It is the abstract base class to hold an attribute, both a leaf and a container (hash/Array...)
@@ -43,7 +43,8 @@ module Attributor
43
43
  result = []
44
44
  size = rand(3) + 1
45
45
  context ||= ["Collection-#{result.object_id}"]
46
-
46
+ context = Array(context)
47
+
47
48
  size.times do |i|
48
49
  subcontext = context + ["at(#{i})"]
49
50
  result << self.member_attribute.example(subcontext)
@@ -84,7 +85,7 @@ module Attributor
84
85
  #puts "Collection: #{self.type}"
85
86
  hash = super(shallow)
86
87
  hash[:options] = {} unless hash[:options]
87
- hash[:options][:member_attribute] = self.member_attribute.describe
88
+ hash[:member_attribute] = self.member_attribute.describe
88
89
  hash
89
90
  end
90
91
 
@@ -9,27 +9,50 @@ module Attributor
9
9
  attr_reader :key_type, :value_type, :options
10
10
  attr_reader :value_attribute
11
11
  attr_reader :key_attribute
12
+ attr_reader :insensitive_map
13
+ attr_accessor :extra_keys
12
14
  end
13
15
 
14
16
  @key_type = Object
15
17
  @value_type = Object
16
-
17
18
 
18
19
  @key_attribute = Attribute.new(@key_type)
19
20
  @value_attribute = Attribute.new(@value_type)
20
21
 
22
+ def self.key_type=(key_type)
23
+ resolved_key_type = Attributor.resolve_type(key_type)
24
+ unless resolved_key_type.ancestors.include?(Attributor::Type)
25
+ raise Attributor::AttributorException.new("Hashes only support key types that are Attributor::Types. Got #{resolved_key_type.name}")
26
+ end
27
+
28
+ @key_type = resolved_key_type
29
+ @key_attribute = Attribute.new(@key_type)
30
+ @concrete=true
31
+ end
32
+
33
+ def self.value_type=(value_type)
34
+ resolved_value_type = Attributor.resolve_type(value_type)
35
+ unless resolved_value_type.ancestors.include?(Attributor::Type)
36
+ raise Attributor::AttributorException.new("Hashes only support value types that are Attributor::Types. Got #{resolved_value_type.name}")
37
+ end
38
+
39
+ @value_type = resolved_value_type
40
+ @value_attribute = Attribute.new(@value_type)
41
+ @concrete=true
42
+ end
43
+
44
+
21
45
  @saved_blocks = []
22
- @options = {}
46
+ @options = {allow_extra: false}
23
47
  @keys = {}
24
48
 
25
-
26
49
  def self.inherited(klass)
27
50
  k = self.key_type
28
51
  v = self.value_type
29
52
 
30
53
  klass.instance_eval do
31
54
  @saved_blocks = []
32
- @options = {}
55
+ @options = {allow_extra: false}
33
56
  @keys = {}
34
57
  @key_type = k
35
58
  @value_type = v
@@ -38,6 +61,10 @@ module Attributor
38
61
  end
39
62
  end
40
63
 
64
+ def self.attributes(**options, &key_spec)
65
+ self.keys(options, &key_spec)
66
+ end
67
+
41
68
  def self.keys(**options, &key_spec)
42
69
  if block_given?
43
70
  @saved_blocks << key_spec
@@ -57,6 +84,12 @@ module Attributor
57
84
  blocks = @saved_blocks.shift(@saved_blocks.size)
58
85
  compiler = dsl_class.new(self, opts)
59
86
  compiler.parse(*blocks)
87
+
88
+ @insensitive_map = self.keys.keys.each_with_object({}) do |k, map|
89
+ map[k.downcase] = k
90
+ end
91
+
92
+ compiler
60
93
  end
61
94
 
62
95
  def self.dsl_class
@@ -73,42 +106,24 @@ module Attributor
73
106
 
74
107
  # @example Hash.of(key: String, value: Integer)
75
108
  def self.of(key: @key_type, value: @value_type)
76
- if key
77
- resolved_key_type = Attributor.resolve_type(key)
78
- unless resolved_key_type.ancestors.include?(Attributor::Type)
79
- raise Attributor::AttributorException.new("Hashes only support key types that are Attributor::Types. Got #{resolved_key_type.name}")
80
- end
81
-
82
- end
83
-
84
- if value
85
- resolved_value_type = Attributor.resolve_type(value)
86
- unless resolved_value_type.ancestors.include?(Attributor::Type)
87
- raise Attributor::AttributorException.new("Hashes only support value types that are Attributor::Types. Got #{resolved_value_type.name}")
88
- end
89
- end
90
-
91
-
92
-
93
-
94
109
  Class.new(self) do
95
- @key_type = resolved_key_type
96
- @value_type = resolved_value_type
97
-
98
- @key_attribute = Attribute.new(@key_type)
99
- @value_attribute = Attribute.new(@value_type)
100
- @concrete = true
110
+ self.key_type = key
111
+ self.value_type = value
101
112
  @keys = {}
102
113
  end
103
114
  end
104
115
 
105
116
 
106
- def self.construct(constructor_block, **options)
117
+ def self.construct(constructor_block, **options)
107
118
  return self if constructor_block.nil?
108
119
 
109
120
  unless @concrete
110
121
  return self.of(key:self.key_type, value: self.value_type)
111
- .construct(constructor_block,**options)
122
+ .construct(constructor_block,**options)
123
+ end
124
+
125
+ if options[:case_insensitive_load] && !(self.key_type <= String)
126
+ raise Attributor::AttributorException.new(":case_insensitive_load may not be used with keys of type #{self.key_type.name}")
112
127
  end
113
128
 
114
129
  self.keys(options, &constructor_block)
@@ -120,7 +135,8 @@ module Attributor
120
135
  return self.new if (key_type == Object && value_type == Object)
121
136
 
122
137
  hash = ::Hash.new
123
- # Let's not bother to generate any hash contents if there's absolutely no type defined
138
+ context ||= ["#{Hash}-#{rand(10000000)}"]
139
+ context = Array(context)
124
140
 
125
141
  if self.keys.any?
126
142
  self.keys.each do |sub_name, sub_attribute|
@@ -128,12 +144,8 @@ module Attributor
128
144
  hash[sub_name] = sub_attribute.example(subcontext)
129
145
  end
130
146
  else
131
-
132
147
  size = rand(3) + 1
133
148
 
134
- context ||= ["#{Hash}-#{rand(10000000)}"]
135
- context = Array(context)
136
-
137
149
  size.times do |i|
138
150
  example_key = key_type.example(context + ["at(#{i})"])
139
151
  subcontext = context + ["at(#{example_key})"]
@@ -158,12 +170,17 @@ module Attributor
158
170
 
159
171
  def self.check_option!(name, definition)
160
172
  case name
161
- when :key_type
162
- :ok
163
- when :value_type
164
- :ok
165
173
  when :reference
166
174
  :ok # FIXME ... actually do something smart
175
+ when :dsl_compiler
176
+ :ok
177
+ when :case_insensitive_load
178
+ unless self.key_type <= String
179
+ raise Attributor::AttributorException, ":case_insensitive_load may not be used with keys of type #{self.key_type.name}"
180
+ end
181
+ :ok
182
+ when :allow_extra
183
+ :ok
167
184
  else
168
185
  :unknown
169
186
  end
@@ -195,23 +212,57 @@ module Attributor
195
212
  end
196
213
 
197
214
  def self.generate_subcontext(context, key_name)
198
- context + ["get(#{key_name.inspect})"]
215
+ context + ["key(#{key_name.inspect})"]
216
+ end
217
+
218
+ def generate_subcontext(context, key_name)
219
+ self.class.generate_sub_context(context,key_name)
220
+ end
221
+
222
+ def set(key, value, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key))
223
+ key = self.class.key_attribute.load(key, context)
224
+
225
+ if (attribute = self.class.keys[key])
226
+ return self[key] = attribute.load(value, context)
227
+ end
228
+
229
+ if self.class.options[:case_insensitive_load]
230
+ key = self.class.insensitive_map[key.downcase]
231
+ return self.set(key, value, context: context)
232
+ end
233
+
234
+ if self.class.options[:allow_extra]
235
+ if self.class.extra_keys.nil?
236
+ return self[key] = self.class.value_attribute.load(value, context)
237
+ else
238
+ return self[self.class.extra_keys].set(key, value, context: context)
239
+ end
240
+ end
241
+
242
+ raise AttributorException, "Unknown key received: #{key.inspect} while loading #{Attributor.humanize_context(context)}"
199
243
  end
200
244
 
201
245
  def self.from_hash(object,context)
202
246
  hash = self.new
203
247
 
204
- object.each do |k,v|
205
- hash_key = @key_type.load(k)
248
+ # if the hash definition includes named extra keys, initialize
249
+ # its value from the object in case it provides some already.
250
+ # this is to ensure it exists when we handle any extra keys
251
+ # that may exist in the object later
252
+ if self.extra_keys
253
+ sub_context = self.generate_subcontext(context,self.extra_keys)
254
+ v = object.fetch(self.extra_keys, {})
255
+ hash.set(self.extra_keys, v, context: sub_context)
256
+ end
206
257
 
207
- hash_attribute = self.keys.fetch(hash_key) do
208
- raise AttributorException, "Unknown key received: #{k.inspect} while loading #{Attributor.humanize_context(context)}"
209
- end
258
+ object.each do |k,v|
259
+ next if k == self.extra_keys
210
260
 
211
- sub_context = self.generate_subcontext(context,hash_key)
212
- hash[hash_key] = hash_attribute.load(v, sub_context)
261
+ sub_context = self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,k)
262
+ hash.set(k, v, context: sub_context)
213
263
  end
214
264
 
265
+ # handle default values for missing keys
215
266
  self.keys.each do |key_name, attribute|
216
267
  next if hash.key?(key_name)
217
268
  sub_context = self.generate_subcontext(context,key_name)
@@ -262,8 +313,6 @@ module Attributor
262
313
 
263
314
  def initialize(contents={})
264
315
  @contents = contents
265
-
266
-
267
316
  end
268
317
 
269
318
  def key_type
@@ -293,7 +342,7 @@ module Attributor
293
342
 
294
343
  if self.class.keys.any?
295
344
  extra_keys = @contents.keys - self.class.keys.keys
296
- if extra_keys.any?
345
+ if extra_keys.any? && !self.class.options[:allow_extra]
297
346
  return extra_keys.collect do |k|
298
347
  "#{Attributor.humanize_context(context)} can not have key: #{k.inspect}"
299
348
  end
@@ -315,12 +364,12 @@ module Attributor
315
364
  # FIXME: the sub contexts and error messages don't really make sense here
316
365
  unless key_type == Attributor::Object
317
366
  sub_context = context + ["key(#{key.inspect})"]
318
- errors.push *key_attribute.validate(key, sub_context)
319
- end
367
+ errors.push *key_attribute.validate(key, sub_context)
368
+ end
320
369
 
321
370
  unless value_type == Attributor::Object
322
371
  sub_context = context + ["value(#{value.inspect})"]
323
- errors.push *value_attribute.validate(value, sub_context)
372
+ errors.push *value_attribute.validate(value, sub_context)
324
373
  end
325
374
  end
326
375
  end
@@ -7,7 +7,7 @@ module Attributor
7
7
  # FIXME: this is not the way to fix this. Really we should add finalize! to Models.
8
8
  undef :timeout
9
9
  undef :format
10
-
10
+ undef :test rescue nil
11
11
 
12
12
  def self.inherited(klass)
13
13
  klass.instance_eval do
@@ -1,3 +1,3 @@
1
1
  module Attributor
2
- VERSION = "2.1.0"
2
+ VERSION = "2.2.0"
3
3
  end
@@ -17,5 +17,3 @@ class IntegerAttributeType
17
17
  end
18
18
 
19
19
  end
20
-
21
-
@@ -282,5 +282,13 @@ describe Attributor::Collection do
282
282
  value.all? { |element| member_type.valid_type?(element) }.should be_true
283
283
  end
284
284
  end
285
+ context "passing a non array context" do
286
+ it 'still is handled correctly ' do
287
+ expect{
288
+ type.example("SimpleString")
289
+ }.to_not raise_error
290
+ end
291
+ end
292
+
285
293
  end
286
294
  end
@@ -6,6 +6,16 @@ describe Attributor::Hash do
6
6
  subject(:type) { Attributor::Hash }
7
7
 
8
8
  its(:native_type) { should be(type) }
9
+ its(:key_type) { should be(Attributor::Object) }
10
+ its(:value_type) { should be(Attributor::Object) }
11
+
12
+ context 'default options' do
13
+ subject(:options) { type.options }
14
+ it 'has allow_extra false' do
15
+ options[:allow_extra].should be(false)
16
+ end
17
+ end
18
+
9
19
 
10
20
  context '.example' do
11
21
  context 'for a simple hash' do
@@ -25,6 +35,17 @@ describe Attributor::Hash do
25
35
  example.values.all? {|v| v.kind_of? Integer}.should be(true)
26
36
  end
27
37
  end
38
+ context 'using a non array context' do
39
+ it 'should work for hashes with key/value types' do
40
+ expect{ Attributor::Hash.of(key:String,value:String).example("Not an Array") }.to_not raise_error
41
+ end
42
+ it 'should work for hashes with keys defined' do
43
+ block = proc { key 'a string', String }
44
+ hash = Attributor::Hash.of(key:String).construct(block)
45
+
46
+ expect{ hash.example("Not an Array") }.to_not raise_error
47
+ end
48
+ end
28
49
  end
29
50
 
30
51
  context '.load' do
@@ -124,7 +145,7 @@ describe Attributor::Hash do
124
145
  subject(:type) { Attributor::Hash.construct(block) }
125
146
 
126
147
  it { should_not be(Attributor::Hash)
127
- }
148
+ }
128
149
 
129
150
  context 'loading' do
130
151
  let(:date) { DateTime.parse("2014-07-15") }
@@ -192,11 +213,18 @@ describe Attributor::Hash do
192
213
  end
193
214
 
194
215
  context '.check_option!' do
195
- it 'accepts key_type:' do
196
- subject.check_option!(:key_type, String).should == :ok
197
- end
198
- it 'accepts value_type' do
199
- subject.check_option!(:value_type, Object).should == :ok
216
+ context ':case_insensitive_load' do
217
+
218
+ it 'is valid when key_type is a string' do
219
+ Attributor::Hash.of(key:String).check_option!(:case_insensitive_load, true).should == :ok
220
+ end
221
+
222
+ it 'is invalid when key_type is non-string' do
223
+ expect {
224
+ Attributor::Hash.of(key:Integer).check_option!(:case_insensitive_load, true)
225
+ }.to raise_error(Attributor::AttributorException, /:case_insensitive_load may not be used/)
226
+ end
227
+
200
228
  end
201
229
  it 'rejects unknown options' do
202
230
  subject.check_option!(:bad_option, Object).should == :unknown
@@ -271,7 +299,7 @@ describe Attributor::Hash do
271
299
  end
272
300
  end
273
301
 
274
- let(:type) { Attributor::Hash.construct(block) }
302
+ let(:type) { Attributor::Hash.construct(block) }
275
303
 
276
304
  let(:values) { {'integer' => 'one', 'datetime' => 'now' } }
277
305
  subject(:hash) { type.new(values) }
@@ -279,7 +307,7 @@ describe Attributor::Hash do
279
307
  it 'validates the keys' do
280
308
  errors = hash.validate
281
309
  errors.should have(3).items
282
- errors.should include("Attribute $.get(\"not-optional\") is required")
310
+ errors.should include("Attribute $.key(\"not-optional\") is required")
283
311
  end
284
312
 
285
313
  end
@@ -369,4 +397,85 @@ describe Attributor::Hash do
369
397
  end
370
398
  end
371
399
 
400
+ context 'with case_insensitive_load option for string keys' do
401
+ let(:block) do
402
+ proc do
403
+ key 'downcase', Integer
404
+ key 'UPCASE', Integer
405
+ key 'CamelCase', Integer
406
+ end
407
+ end
408
+
409
+ let(:type) { Attributor::Hash.of(key: String).construct(block, case_insensitive_load: true) }
410
+
411
+ let(:input) { {'DOWNCASE' => 1, 'upcase' => 2, 'CamelCase' => 3} }
412
+
413
+ subject(:output) { type.load(input) }
414
+
415
+ it 'maps the incoming keys to defined keys, regardless of case' do
416
+ output['downcase'].should eq(1)
417
+ output['UPCASE'].should eq(2)
418
+ output['CamelCase'].should eq(3)
419
+ end
420
+
421
+ end
422
+
423
+ context 'with allow_extra keys option' do
424
+ let(:type) do
425
+ Class.new(Attributor::Hash) do
426
+ self.value_type = String
427
+
428
+ keys allow_extra: true do
429
+ key :one, String
430
+ key :two, String, default: 'two'
431
+ end
432
+ end
433
+ end
434
+
435
+ let(:input) { {one: 'one', three: 3} }
436
+ subject(:output) { type.load(input) }
437
+
438
+ context 'that should be saved at the top level' do
439
+ its(:keys) { should =~ [:one, :two, :three] }
440
+
441
+ it 'loads the extra keys' do
442
+ output[:one].should eq('one')
443
+ output[:two].should eq('two')
444
+ output[:three].should eq('3')
445
+ end
446
+
447
+ its( :validate ){ should be_empty }
448
+ end
449
+
450
+ context 'that should be grouped into a sub-hash' do
451
+ before do
452
+ type.keys do
453
+ extra :options, Attributor::Hash.of(value: Integer)
454
+ end
455
+ end
456
+
457
+ its(:keys) { should =~ [:one, :two, :options] }
458
+ it 'loads the extra keys into :options sub-hash' do
459
+ output[:one].should eq('one')
460
+ output[:two].should eq('two')
461
+ output[:options].should eq({three: 3})
462
+ end
463
+ its( :validate ){ should be_empty }
464
+
465
+ context 'with an options value already provided' do
466
+ its(:keys) { should =~ [:one, :two, :options] }
467
+ let(:input) { {one: 'one', three: 3, options: {four: 4}} }
468
+
469
+ it 'loads the extra keys into :options sub-hash' do
470
+ output[:one].should eq('one')
471
+ output[:two].should eq('two')
472
+ output[:options].should eq({three: 3, four: 4})
473
+ end
474
+ its( :validate ){ should be_empty }
475
+
476
+ end
477
+ end
478
+
479
+ end
480
+
372
481
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attributor
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep M. Blanquer
@@ -318,7 +318,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
318
318
  version: '0'
319
319
  requirements: []
320
320
  rubyforge_project:
321
- rubygems_version: 2.2.1
321
+ rubygems_version: 2.2.2
322
322
  signing_key:
323
323
  specification_version: 4
324
324
  summary: A powerful attribute and type management library for Ruby