attributor 2.5.0 → 2.6.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: 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