attributor 2.1.0 → 2.2.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 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