attributor 2.5.0 → 2.6.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: 6b7e1ce2db7e093c7a3073db2cea1d45b861f015
4
- data.tar.gz: f3623a6d5e3b313b9493cba77a6e7a7b250d1de5
3
+ metadata.gz: b366557c378f786a7a4620793c05bdb06504ef3e
4
+ data.tar.gz: 728171cc3c89f03c7f998fb24db74fada6c845a8
5
5
  SHA512:
6
- metadata.gz: 625ac84d1636b6d21044c69d8be170bae210b28f302c2ed78de5b3130e1d82e760ee65c68a6b9f0c614e06b8a2a4770ab26da92a6b5917d847a1a02a73bdb522
7
- data.tar.gz: f7c4324913a80c92191c9bc6f46ced3d5f7e9f01758faea751c79254b24e547f68b88ce836c446523cd759e2c35d4ca04778b7c033787878b5f75e88b77d7046
6
+ metadata.gz: facbc89fd9ac8b18cd60611b3699bf3308854890becc782522959497d81c6c89abf59dbc29219c5c9e378a8ff317bfb67b511696c377bd6c024ad234ba3f5866
7
+ data.tar.gz: 0abb9af1bde8c73bf374686aeb441ef6aba3426f9767970a072d2730d6e71b3e164c9458d1bcb42a48851f424bfdff302096a78300bf8758c0b5461743a13e8f
data/.travis.yml CHANGED
@@ -1,4 +1,8 @@
1
+ sudo: false
1
2
  language: ruby
2
3
  rvm:
3
- - "2.1.2"
4
+ - "2.1.5"
4
5
  script: bundle exec rspec spec
6
+ branches:
7
+ only:
8
+ - master
data/CHANGELOG.md CHANGED
@@ -1,8 +1,18 @@
1
1
  Attributor Changelog
2
2
  ============================
3
3
 
4
- next
5
- ----
4
+ 2.6.0
5
+ -----
6
+
7
+ * Fixed bug in `example_mixin` where lazy_attributes were not evaluated.
8
+ * Fixed bug in `Hash` where the class would refuse to load from another `Attributor::Hash` when there were no keys defined and they were seemingly compatible.
9
+ * Fixed a `Hash.dump` bug where nil attribute values would transitively be `dumpe`d therefore causing a nil dereference.
10
+ * Hardened the `dump`ing of types to support nil values.
11
+ * Fix `attribute.example` to actually accept native types (that are not only Strings)
12
+ * Fixed bug where `Hash#get` would insert a nil value if asked for a key that was not present in the hash.
13
+ * Fixed bug in `Hash.from_hash` where it would add nil values for keys that are defined on the type but not present in the input.
14
+ * Added `Hash#merge` that works with two identically-typed hashes
15
+ * Added `Hash#each_pair` for better duck-type compatibility with ::Hash.
6
16
 
7
17
  2.5.0
8
18
  ----
data/README.md CHANGED
@@ -22,23 +22,23 @@ With Attributor you can:
22
22
 
23
23
  ### Running specs:
24
24
 
25
- `bundle exec rake spec`
25
+ bundle exec rake spec
26
26
 
27
27
  Note: This should also compute code coverage. See below for details on viewing code coverage.
28
28
 
29
29
  ### Generating documentation:
30
30
 
31
- `bundle exec yard`
31
+ bundle exec yard
32
32
 
33
33
  ### Computing documentation coverage:
34
34
 
35
- `bundle exec yardstick 'lib/**/*.rb'`
35
+ bundle exec yardstick 'lib/**/*.rb'
36
36
 
37
37
  ### Computing code coverage:
38
38
 
39
- `bundle exec rake spec`
39
+ bundle exec rake spec
40
40
 
41
- `open coverage/index.html`
41
+ open coverage/index.html
42
42
 
43
43
 
44
44
  ## Contributing to attributor
@@ -135,9 +135,6 @@ module Attributor
135
135
  if self.options.has_key? :example
136
136
  val = self.options[:example]
137
137
  case val
138
- when ::String
139
- # FIXME: spec this properly to use self.type.native_type
140
- val
141
138
  when ::Regexp
142
139
  self.load(val.gen,context)
143
140
  when ::Array
@@ -154,7 +151,7 @@ module Attributor
154
151
  when nil
155
152
  nil
156
153
  else
157
- raise AttributorException, "unknown example attribute type, got: #{val}"
154
+ self.load(val)
158
155
  end
159
156
  else
160
157
  if (option_values = self.options[:values])
@@ -5,6 +5,7 @@ module Attributor
5
5
 
6
6
  class AttributeResolver
7
7
  ROOT_PREFIX = '$'.freeze
8
+ COLLECTION_INDEX_KEY = /^at\((\d+)\)$/.freeze
8
9
 
9
10
  class Data < ::Hash
10
11
  include Hashie::Extensions::MethodReader
@@ -17,7 +18,6 @@ module Attributor
17
18
  end
18
19
 
19
20
 
20
- # TODO: support collection queries
21
21
  def query!(key_path, path_prefix=ROOT_PREFIX)
22
22
  # If the incoming key_path is not an absolute path, append the given prefix
23
23
  # NOTE: Need to index key_path by range here because Ruby 1.8 returns a
@@ -30,12 +30,21 @@ module Attributor
30
30
  # Discard the initial element, which should always be ROOT_PREFIX at this point
31
31
  _root, *path = key_path.split(SEPARATOR)
32
32
 
33
- # Follow the hierarchy path to the requested node and return it
33
+ # Follow the hierarchy path to the requested node and return it:
34
34
  # Example path => ["instance", "ssh_key", "name"]
35
35
  # Example @data => {"instance" => { "ssh_key" => { "name" => "foobar" } }}
36
+ #
37
+ # at(n) is a collection index:
38
+ # Example path => ["filters", "at(0)", "type"]
39
+ # Example data => {"filters" => [{ "type" => "instance:tag" }]}
40
+ #
36
41
  result = path.inject(@data) do |hash, key|
37
42
  return nil if hash.nil?
38
- hash.send key
43
+ if (match = key.match(COLLECTION_INDEX_KEY))
44
+ hash[match[1].to_i]
45
+ else
46
+ hash.send key
47
+ end
39
48
  end
40
49
  result
41
50
  end
@@ -51,7 +51,7 @@ module Attributor
51
51
  end
52
52
 
53
53
  def contents
54
- lazy_attributes.keys do |key|
54
+ lazy_attributes.keys.each do |key|
55
55
  proc = lazy_attributes.delete(key)
56
56
  @contents[key] = proc.call(self)
57
57
  end
@@ -12,9 +12,11 @@ module Attributor
12
12
  values
13
13
  when ::Array
14
14
  values.collect { |value| member_attribute.dump(value,opts).to_s }.join(',')
15
+ when nil
16
+ nil
15
17
  else
16
- context = opts[:context]
17
- name = opts[:context].last.to_s
18
+ context = opts[:context] || DEFAULT_ROOT_CONTEXT
19
+ name = context.last.to_s
18
20
  type = values.class.name
19
21
  reason = "Attributor::CSV only supports dumping values of type " +
20
22
  "Array or String, not #{values.class.name}."
@@ -32,7 +32,7 @@ module Attributor
32
32
  end
33
33
 
34
34
  def self.dump(value,**opts)
35
- value.iso8601
35
+ value && value.iso8601
36
36
  end
37
37
 
38
38
  end
@@ -31,7 +31,7 @@ module Attributor
31
31
  end
32
32
 
33
33
  def self.dump(value,**opts)
34
- value.iso8601
34
+ value && value.iso8601
35
35
  end
36
36
 
37
37
 
@@ -1,6 +1,5 @@
1
1
  module Attributor
2
2
  class Hash
3
- extend Forwardable
4
3
 
5
4
  MAX_EXAMPLE_DEPTH = 5
6
5
  CIRCULAR_REFERENCE_MARKER = '...'.freeze
@@ -157,7 +156,7 @@ module Attributor
157
156
 
158
157
  def self.example(context=nil, **values)
159
158
  if (key_type == Object && value_type == Object && self.keys.empty?)
160
- return self.new
159
+ return self.new
161
160
  end
162
161
 
163
162
  context ||= ["#{Hash}-#{rand(10000000)}"]
@@ -185,8 +184,11 @@ module Attributor
185
184
 
186
185
 
187
186
  def self.dump(value, **opts)
188
- self.load(value).
189
- dump(**opts)
187
+ if loaded = self.load(value)
188
+ loaded.dump(**opts)
189
+ else
190
+ nil
191
+ end
190
192
  end
191
193
 
192
194
 
@@ -221,10 +223,6 @@ module Attributor
221
223
  elsif value.is_a?(self)
222
224
  return value
223
225
  elsif value.kind_of?(Attributor::Hash)
224
- if (value.keys - self.attributes.keys).any?
225
- raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
226
- end
227
-
228
226
  loaded_value = value.contents
229
227
  elsif value.is_a?(::Hash)
230
228
  loaded_value = value
@@ -255,11 +253,17 @@ module Attributor
255
253
 
256
254
  def get(key, context: self.generate_subcontext(Attributor::DEFAULT_ROOT_CONTEXT,key))
257
255
  key = self.class.key_attribute.load(key, context)
258
-
256
+
259
257
  value = @contents[key]
260
258
 
259
+ # FIXME: getting an unset value here should not force it in the hash
261
260
  if (attribute = self.class.keys[key])
262
- return self[key] = attribute.load(value, context)
261
+ loaded_value = attribute.load(value, context)
262
+ if loaded_value.nil?
263
+ return nil
264
+ else
265
+ return self[key] = loaded_value
266
+ end
263
267
  end
264
268
 
265
269
  if self.class.options[:case_insensitive_load]
@@ -338,7 +342,8 @@ module Attributor
338
342
  self.keys.each do |key_name, attribute|
339
343
  next if hash.key?(key_name)
340
344
  sub_context = self.generate_subcontext(context,key_name)
341
- hash[key_name] = attribute.load(nil, sub_context, recurse: recurse)
345
+ default = attribute.load(nil, sub_context, recurse: recurse)
346
+ hash[key_name] = default unless default.nil?
342
347
  end
343
348
 
344
349
  hash
@@ -379,17 +384,54 @@ module Attributor
379
384
 
380
385
  # TODO: Think about the format of the subcontexts to use: let's use .at(key.to_s)
381
386
  attr_reader :contents
382
-
383
- def_delegators :@contents,
384
- :[],
385
- :[]=,
386
- :each,
387
- :size,
388
- :keys,
389
- :key?,
390
- :values,
391
- :empty?,
392
- :has_key?
387
+
388
+ def [](k)
389
+ @contents[k]
390
+ end
391
+
392
+ def []=(k,v)
393
+ @contents[k] = v
394
+ end
395
+
396
+ def each(&block)
397
+ @contents.each(&block)
398
+ end
399
+
400
+ def each_pair(&block)
401
+ @contents.each_pair(&block)
402
+ end
403
+
404
+ def size
405
+ @contents.size
406
+ end
407
+
408
+ def keys
409
+ @contents.keys
410
+ end
411
+
412
+ def values
413
+ @contents.values
414
+ end
415
+
416
+ def empty?
417
+ @contents.empty?
418
+ end
419
+
420
+ def key?(k)
421
+ @contents.key?(k)
422
+ end
423
+ alias_method :has_key?, :key?
424
+
425
+ def merge(h)
426
+ case h
427
+ when self.class
428
+ self.class.new(@contents.merge(h.contents))
429
+ when Attributor::Hash
430
+ raise ArgumentError, "cannot merge Attributor::Hash instances of different types" unless h.is_a?(self.class)
431
+ else
432
+ raise TypeError, "no implicit conversion of #{h.class} into Attributor::Hash"
433
+ end
434
+ end
393
435
 
394
436
  attr_reader :validating, :dumping
395
437
 
@@ -13,7 +13,7 @@ module Attributor
13
13
  end
14
14
 
15
15
  def self.dump(value, **opts)
16
- value.path
16
+ value && value.path
17
17
  end
18
18
 
19
19
  def self.load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
@@ -34,7 +34,7 @@ module Attributor
34
34
  end
35
35
 
36
36
  def self.dump(value,**opts)
37
- value.iso8601
37
+ value && value.iso8601
38
38
  end
39
39
 
40
40
 
@@ -1,3 +1,3 @@
1
1
  module Attributor
2
- VERSION = "2.5.0"
2
+ VERSION = "2.6.0"
3
3
  end
@@ -66,6 +66,37 @@ describe Attributor::AttributeResolver do
66
66
  end
67
67
  end
68
68
 
69
+ context 'querying collection indices from models' do
70
+ let(:instances) { [instance1, instance2] }
71
+ let(:instance1) { double('instance1', :ssh_key => ssh_key1) }
72
+ let(:instance2) { double('instance2', :ssh_key => ssh_key2) }
73
+ let(:ssh_key1) { double('ssh_key', :name => value) }
74
+ let(:ssh_key2) { double('ssh_key', :name => 'second') }
75
+ let(:args) { [path, prefix].compact }
76
+
77
+ before { subject.register('instances', instances) }
78
+
79
+ it 'resolves the index to the correct member of the collection' do
80
+ subject.query('instances').should be instances
81
+ subject.query('instances.at(1).ssh_key').should be ssh_key2
82
+ subject.query('instances.at(0).ssh_key.name').should be value
83
+ end
84
+
85
+ it 'returns nil for index out of range' do
86
+ subject.query('instances.at(2)').should be(nil)
87
+ subject.query('instances.at(-1)').should be(nil)
88
+ end
89
+
90
+ context 'with a prefix' do
91
+ let(:key) { 'name' }
92
+ let(:prefix) { '$.instances.at(0).ssh_key'}
93
+ let(:value) { 'some_name' }
94
+
95
+ it 'resolves the index to the correct member of the collection' do
96
+ subject.query(key, prefix).should be(value)
97
+ end
98
+ end
99
+ end
69
100
 
70
101
  context 'checking attribute conditions' do
71
102
  let(:key) { "instance.ssh_key.name" }
@@ -123,7 +154,7 @@ describe Attributor::AttributeResolver do
123
154
  end
124
155
  end
125
156
 
126
- context 'with a hash condition' do
157
+ pending 'with a hash condition' do
127
158
  end
128
159
 
129
160
  context 'with a proc condition' do
@@ -146,12 +146,21 @@ describe Attributor::Attribute do
146
146
 
147
147
  context 'for a type with a non-String native_type' do
148
148
  let(:type) { IntegerAttributeType}
149
- let(:example) { /\d{5}/ }
150
- it 'coerces the example value properly' do
151
- example.should_receive(:gen).and_call_original
152
- type.should_receive(:load).and_call_original
153
-
154
- subject.example.should be_kind_of(type.native_type)
149
+ context 'using a regexp' do
150
+ let(:example) { /\d{5}/ }
151
+ it 'coerces the example value properly' do
152
+ example.should_receive(:gen).and_call_original
153
+ type.should_receive(:load).and_call_original
154
+
155
+ subject.example.should be_kind_of(type.native_type)
156
+ end
157
+ end
158
+ context 'usign a native Integer type' do
159
+ let(:example) { 5 }
160
+ it 'coerces the example value properly' do
161
+ type.should_receive(:load).and_call_original
162
+ subject.example.should be_kind_of(type.native_type)
163
+ end
155
164
  end
156
165
  end
157
166
  end
@@ -152,7 +152,7 @@ describe Attributor::Collection do
152
152
  it "raises error when incoming value is not of member_type" do
153
153
  expect {
154
154
  val = type.of(member_type).load(value)
155
- }.to raise_error(Attributor::AttributorException)
155
+ }.to raise_error(Attributor::AttributorException,/Unknown key received/)
156
156
  end
157
157
 
158
158
  end
@@ -43,6 +43,10 @@ describe Attributor::CSV do
43
43
  it 'dumps non-Integer values also' do
44
44
  csv.dump(str_vals).should eq(str_vals.join(','))
45
45
  end
46
+
47
+ it 'dumps nil values as nil' do
48
+ csv.dump(nil).should eq(nil)
49
+ end
46
50
  end
47
51
 
48
52
  end
@@ -18,6 +18,11 @@ describe Attributor::Date do
18
18
  it 'is formatted correctly' do
19
19
  value.should match(/\d{4}-\d{2}-\d{2}T00:00:00\+00:00/)
20
20
  end
21
+ context 'nil values' do
22
+ it 'should be nil' do
23
+ type.dump(nil).should be_nil
24
+ end
25
+ end
21
26
  end
22
27
 
23
28
 
@@ -18,6 +18,11 @@ describe Attributor::DateTime do
18
18
  it 'is formatted correctly' do
19
19
  value.should match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[\+-]\d{2}:\d{2}/)
20
20
  end
21
+ context 'nil values' do
22
+ it 'should be nil' do
23
+ type.dump(nil).should be_nil
24
+ end
25
+ end
21
26
  end
22
27
 
23
28
 
@@ -110,6 +110,82 @@ describe Attributor::Hash do
110
110
  end
111
111
  end
112
112
  end
113
+
114
+ context 'for another Attributor Hash with a compatible type definition' do
115
+ let(:other_hash) do
116
+ Attributor::Hash.of(key: Integer, value: Integer)
117
+ end
118
+ let(:value) { other_hash.example }
119
+ it 'succeeds' do
120
+ type.load(value)
121
+ end
122
+ end
123
+
124
+ context 'for Hash with defined keys' do
125
+ let(:type) do
126
+ Class.new(Attributor::Hash) do
127
+ keys do
128
+ key 'id', Integer
129
+ key 'name', String, default: "unnamed"
130
+ key 'chicken', Chicken
131
+ end
132
+ end
133
+ end
134
+
135
+ let(:value) { {'chicken' => Chicken.example} }
136
+
137
+ subject(:hash) { type.load(value) }
138
+
139
+ it { should_not have_key('id') }
140
+ it 'has the defaulted key' do
141
+ hash.should have_key('name')
142
+ hash['name'].should eq('unnamed')
143
+ end
144
+ end
145
+
146
+ context 'for a different Attributor Hash' do
147
+ let(:loader_hash) do
148
+ Class.new(Attributor::Hash) do
149
+ keys do
150
+ key :id, String
151
+ key :name, String
152
+ end
153
+ end
154
+ end
155
+ let(:value) { value_hash.example }
156
+ context 'with compatible key definitions' do
157
+ let(:value_hash) do
158
+ Class.new(Attributor::Hash) do
159
+ keys do
160
+ key :id, String
161
+ end
162
+ end
163
+ end
164
+
165
+ it 'succeeds' do
166
+ loader_hash.load(value)
167
+ end
168
+
169
+ context 'with a not compatible key definition' do
170
+ let(:value_hash) do
171
+ Class.new(Attributor::Hash) do
172
+ keys do
173
+ key :id, String
174
+ key :weird_key, String
175
+ end
176
+ end
177
+ end
178
+
179
+ it 'complains about an unknown key' do
180
+ expect {
181
+ loader_hash.load(value)
182
+ }.to raise_error(Attributor::AttributorException,/Unknown key received: :weird_key/)
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+
113
189
  end
114
190
 
115
191
 
@@ -254,6 +330,9 @@ describe Attributor::Hash do
254
330
  end
255
331
 
256
332
  context 'for a typed hash' do
333
+ before do
334
+ subtype.should_receive(:dump).exactly(2).times.and_call_original
335
+ end
257
336
  let(:value1) { {first: "Joe", last: "Moe"} }
258
337
  let(:value2) { {first: "Mary", last: "Foe"} }
259
338
  let(:value) { { id1: subtype.new(value1), id2: subtype.new(value2) } }
@@ -268,8 +347,6 @@ describe Attributor::Hash do
268
347
  let(:type) { Attributor::Hash.of(key: String, value: subtype) }
269
348
 
270
349
  it 'returns a hash with the dumped values and keys' do
271
- subtype.should_receive(:dump).exactly(2).times.and_call_original
272
-
273
350
  dumped_value = type.dump(value, opts)
274
351
  dumped_value.should be_kind_of(::Hash)
275
352
  dumped_value.keys.should =~ ['id1','id2']
@@ -278,6 +355,17 @@ describe Attributor::Hash do
278
355
  dumped_value['id2'].should == value2
279
356
  end
280
357
 
358
+ context 'that has nil attribute values' do
359
+ let(:value) { { id1: nil, id2: subtype.new(value2) } }
360
+
361
+ it 'correctly returns nil rather than trying to dump their contents' do
362
+ dumped_value = type.dump(value, opts)
363
+ dumped_value.should be_kind_of(::Hash)
364
+ dumped_value.keys.should =~ ['id1','id2']
365
+ dumped_value['id1'].should == nil
366
+ dumped_value['id2'].should == value2
367
+ end
368
+ end
281
369
  end
282
370
 
283
371
  end
@@ -593,10 +681,41 @@ describe Attributor::Hash do
593
681
 
594
682
  hash.get('foo').should be(bar)
595
683
  end
684
+
685
+ it 'does not set a key that is unset' do
686
+ hash.should_not have_key('id')
687
+ hash.get('id').should be(nil)
688
+ hash.should_not have_key('id')
689
+ end
690
+
596
691
  end
597
692
 
598
693
  end
599
694
 
600
695
  end
601
696
 
697
+ context '#merge' do
698
+ let(:hash_of_strings) { Attributor::Hash.of(key: String) }
699
+ let(:hash_of_symbols) { Attributor::Hash.of(key: Symbol) }
700
+
701
+ let(:merger) { hash_of_strings.load('a' => 1) }
702
+ let(:good_mergee) { hash_of_strings.load('b' => 2) }
703
+ let(:bad_mergee) { hash_of_symbols.load(c: 3) }
704
+ let(:result) { hash_of_strings.load('a' => 1, 'b' => 2) }
705
+
706
+ it 'validates that the mergee is of like type' do
707
+ expect { merger.merge(bad_mergee) }.to raise_error(ArgumentError)
708
+ expect { merger.merge({}) }.to raise_error(TypeError)
709
+ expect { merger.merge(nil) }.to raise_error(TypeError)
710
+ end
711
+
712
+ it 'returns a like-typed result' do
713
+ expect(merger.merge(good_mergee)).to be_a(hash_of_strings)
714
+ end
715
+
716
+ it 'merges' do
717
+ expect(merger.merge(good_mergee)).to eq(result)
718
+ end
719
+
720
+ end
602
721
  end
@@ -172,7 +172,7 @@ describe Attributor::Model do
172
172
  expect {
173
173
  turducken = Turducken.example
174
174
  chicken = Chicken.load(turducken,context)
175
- }.to raise_error(Attributor::IncompatibleTypeError, /Type Chicken cannot load values of type Turducken.*#{context.join('.')}/)
175
+ }.to raise_error(Attributor::AttributorException, /Unknown key received/)
176
176
  end
177
177
  end
178
178
 
@@ -18,6 +18,11 @@ describe Attributor::Time do
18
18
  it 'is formatted correctly' do
19
19
  value.should match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[\+-]\d{2}:\d{2}/)
20
20
  end
21
+ context 'nil values' do
22
+ it 'should be nil' do
23
+ type.dump(nil).should be_nil
24
+ end
25
+ end
21
26
  end
22
27
 
23
28
 
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.5.0
4
+ version: 2.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josep M. Blanquer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-02-11 00:00:00.000000000 Z
12
+ date: 2015-03-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: hashie