attributor 5.4 → 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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/lib/attributor/attribute.rb +101 -84
  4. data/lib/attributor/extras/field_selector.rb +4 -0
  5. data/lib/attributor/families/numeric.rb +19 -6
  6. data/lib/attributor/families/temporal.rb +16 -9
  7. data/lib/attributor/hash_dsl_compiler.rb +6 -5
  8. data/lib/attributor/type.rb +26 -3
  9. data/lib/attributor/types/bigdecimal.rb +6 -1
  10. data/lib/attributor/types/boolean.rb +5 -0
  11. data/lib/attributor/types/collection.rb +19 -0
  12. data/lib/attributor/types/csv.rb +4 -0
  13. data/lib/attributor/types/date.rb +7 -1
  14. data/lib/attributor/types/date_time.rb +7 -1
  15. data/lib/attributor/types/float.rb +4 -3
  16. data/lib/attributor/types/hash.rb +86 -23
  17. data/lib/attributor/types/integer.rb +7 -1
  18. data/lib/attributor/types/model.rb +9 -21
  19. data/lib/attributor/types/object.rb +5 -0
  20. data/lib/attributor/types/polymorphic.rb +0 -1
  21. data/lib/attributor/types/string.rb +19 -0
  22. data/lib/attributor/types/symbol.rb +5 -0
  23. data/lib/attributor/types/tempfile.rb +4 -0
  24. data/lib/attributor/types/time.rb +6 -2
  25. data/lib/attributor/types/uri.rb +8 -0
  26. data/lib/attributor/version.rb +1 -1
  27. data/lib/attributor.rb +3 -7
  28. data/spec/attribute_spec.rb +148 -124
  29. data/spec/extras/field_selector/field_selector_spec.rb +9 -0
  30. data/spec/hash_dsl_compiler_spec.rb +5 -5
  31. data/spec/spec_helper.rb +0 -2
  32. data/spec/support/integers.rb +7 -0
  33. data/spec/support/models.rb +7 -7
  34. data/spec/types/bigdecimal_spec.rb +8 -0
  35. data/spec/types/boolean_spec.rb +10 -0
  36. data/spec/types/collection_spec.rb +16 -0
  37. data/spec/types/date_spec.rb +9 -0
  38. data/spec/types/date_time_spec.rb +9 -0
  39. data/spec/types/float_spec.rb +8 -0
  40. data/spec/types/hash_spec.rb +181 -22
  41. data/spec/types/integer_spec.rb +9 -0
  42. data/spec/types/model_spec.rb +7 -1
  43. data/spec/types/string_spec.rb +10 -0
  44. data/spec/types/temporal_spec.rb +5 -1
  45. data/spec/types/time_spec.rb +9 -0
  46. data/spec/types/uri_spec.rb +9 -0
  47. metadata +5 -6
  48. data/lib/attributor/attribute_resolver.rb +0 -111
  49. data/spec/attribute_resolver_spec.rb +0 -237
@@ -7,6 +7,7 @@ describe Attributor::Hash do
7
7
  its(:key_type) { should be(Attributor::Object) }
8
8
  its(:value_type) { should be(Attributor::Object) }
9
9
  its(:dsl_class) { should be(Attributor::HashDSLCompiler) }
10
+ its(:json_schema_type) { should be(:object) }
10
11
 
11
12
  context 'attributes' do
12
13
  context 'with an exception from the definition block' do
@@ -513,6 +514,7 @@ describe Attributor::Hash do
513
514
  end
514
515
  end
515
516
  end
517
+
516
518
  context '#validate' do
517
519
  context 'for a key and value typed hash' do
518
520
  let(:key_type) { Integer }
@@ -533,45 +535,85 @@ describe Attributor::Hash do
533
535
  context 'for a hash with defined keys' do
534
536
  let(:block) do
535
537
  proc do
536
- key 'integer', Integer
538
+ key 'integer', Integer, null: false # non-nullable, but not required
537
539
  key 'datetime', DateTime
538
- key 'not-optional', String, required: true
540
+ key 'not-optional', String, required: true, null: false # required AND non-nullable
541
+ key 'required-but-nullable', String, required: true, null: true
539
542
  end
540
543
  end
541
544
 
542
545
  let(:type) { Attributor::Hash.construct(block) }
543
-
544
- let(:values) { { 'integer' => 'one', 'datetime' => 'now' } }
545
546
  subject(:hash) { type.new(values) }
546
547
 
547
- it 'validates the keys' do
548
- errors = hash.validate
549
- expect(errors).to have(3).items
550
- expect(errors).to include('Attribute $.key("not-optional") is required')
548
+ context 'validates it all' do
549
+ let(:values) { { 'integer' => 'one', 'datetime' => 'now', 'required-but-nullable' => nil} }
550
+ it 'validates the keys' do
551
+ errors = hash.validate
552
+ expect(errors).to have(3).items
553
+ [
554
+ 'Attribute $.key("integer") received value: "one" is of the wrong type (got: String, expected: Attributor::Integer)',
555
+ 'Attribute $.key("datetime") received value: "now" is of the wrong type (got: String, expected: Attributor::DateTime)',
556
+ 'Attribute $.key("not-optional") is required'
557
+ ].each do |msg|
558
+ regexp = Regexp.new(Regexp.escape(msg))
559
+ expect(errors.any?{|err| err =~ regexp}).to be_truthy
560
+ end
561
+ end
562
+ end
563
+
564
+ context 'still validates requiredness even if values are nullable' do
565
+ let(:values) { {} }
566
+ it 'complains if not provided' do
567
+ errors = hash.validate
568
+ expect(errors).to have(2).items
569
+ [
570
+ 'Attribute $.key("not-optional") is required',
571
+ 'Attribute $.key("required-but-nullable") is required'
572
+ ].each do |msg|
573
+ regexp = Regexp.new(Regexp.escape(msg))
574
+ expect(errors.any?{|err| err =~ regexp}).to be_truthy
575
+ end
576
+ end
551
577
  end
578
+
579
+ context 'validates nullability regardless of requiredness' do
580
+ let(:values) { {'integer' => nil, 'not-optional' => nil, 'required-but-nullable' => nil} }
581
+ it 'complains if null' do
582
+ errors = hash.validate
583
+ expect(errors).to have(2).items
584
+ [
585
+ 'Attribute $.key("integer") is not nullable',
586
+ 'Attribute $.key("not-optional") is not nullable',
587
+ ].each do |msg|
588
+ regexp = Regexp.new(Regexp.escape(msg))
589
+ expect(errors.any?{|err| err =~ regexp}).to be_truthy
590
+ end
591
+ end
592
+ end
593
+
552
594
  end
553
595
 
554
596
  context 'with requirements defined' do
555
597
  let(:type) { Attributor::Hash.construct(block) }
556
598
 
557
- context 'using requires' do
599
+ context 'using the requires DSL' do
558
600
  let(:block) do
559
601
  proc do
560
602
  key 'name', String
603
+ key 'nevernull', String, null: false
561
604
  key 'consistency', Attributor::Boolean
562
- key 'availability', Attributor::Boolean
563
- key 'partitioning', Attributor::Boolean
605
+ key 'availability', Attributor::Boolean, null: true
606
+ key 'partitioning', Attributor::Boolean, null: true
564
607
  requires 'consistency', 'availability'
565
- requires.all 'name' # Just to show that it is equivalent to 'requires'
608
+ requires.all 'name'
566
609
  end
567
610
  end
568
611
 
569
612
  it 'complains not all the listed elements are set (false or true)' do
570
- errors = type.new('name' => 'CAP').validate
613
+ errors = type.new('name' => 'CAP', 'consistency' => true, 'nevernull' => nil).validate
571
614
  expect(errors).to have(2).items
572
- %w(consistency availability).each do |name|
573
- expect(errors).to include("Key #{name} is required for $.")
574
- end
615
+ expect(errors).to include('Attribute $.key("nevernull") is not nullable.')
616
+ expect(errors).to include('Attribute $.key("availability") is required.')
575
617
  end
576
618
  end
577
619
 
@@ -590,7 +632,7 @@ describe Attributor::Hash do
590
632
  errors = type.new('name' => 'CAP', 'consistency' => false).validate
591
633
  expect(errors).to have(1).items
592
634
  expect(errors).to include(
593
- 'At least 2 keys out of ["consistency", "availability", "partitioning"] are required to be passed in for $. Found ["consistency"]'
635
+ 'At least 2 attributes out of ["consistency", "availability", "partitioning"] are required to be passed in for $. Found ["consistency"]'
594
636
  )
595
637
  end
596
638
  end
@@ -609,7 +651,7 @@ describe Attributor::Hash do
609
651
  it 'complains if more than 2 in the group are set (false or true)' do
610
652
  errors = type.new('name' => 'CAP', 'consistency' => false, 'availability' => true, 'partitioning' => false).validate
611
653
  expect(errors).to have(1).items
612
- expect(errors).to include('At most 2 keys out of ["consistency", "availability", "partitioning"] can be passed in for $. Found ["consistency", "availability", "partitioning"]')
654
+ expect(errors).to include('At most 2 attributes out of ["consistency", "availability", "partitioning"] can be passed in for $. Found ["consistency", "availability", "partitioning"]')
613
655
  end
614
656
  end
615
657
 
@@ -627,12 +669,12 @@ describe Attributor::Hash do
627
669
  it 'complains if less than 1 in the group are set (false or true)' do
628
670
  errors = type.new('name' => 'CAP').validate
629
671
  expect(errors).to have(1).items
630
- expect(errors).to include('Exactly 1 of the following keys ["consistency", "availability", "partitioning"] are required for $. Found 0 instead: []')
672
+ expect(errors).to include('Exactly 1 of the following attributes ["consistency", "availability", "partitioning"] are required for $. Found 0 instead: []')
631
673
  end
632
674
  it 'complains if more than 1 in the group are set (false or true)' do
633
675
  errors = type.new('name' => 'CAP', 'consistency' => false, 'availability' => true).validate
634
676
  expect(errors).to have(1).items
635
- expect(errors).to include('Exactly 1 of the following keys ["consistency", "availability", "partitioning"] are required for $. Found 2 instead: ["consistency", "availability"]')
677
+ expect(errors).to include('Exactly 1 of the following attributes ["consistency", "availability", "partitioning"] are required for $. Found 2 instead: ["consistency", "availability"]')
636
678
  end
637
679
  end
638
680
 
@@ -650,7 +692,7 @@ describe Attributor::Hash do
650
692
  it 'complains if two or more in the group are set (false or true)' do
651
693
  errors = type.new('name' => 'CAP', 'consistency' => false, 'availability' => true).validate
652
694
  expect(errors).to have(1).items
653
- expect(errors).to include('keys ["consistency", "availability"] are mutually exclusive for $.')
695
+ expect(errors).to include('Attributes ["consistency", "availability"] are mutually exclusive for $.')
654
696
  end
655
697
  end
656
698
 
@@ -675,7 +717,7 @@ describe Attributor::Hash do
675
717
  errors = type.new('name' => 'CAP').validate
676
718
  expect(errors).to have(1).items
677
719
  expect(errors).to include(
678
- 'At least 1 keys out of ["consistency", "availability", "partitioning"] are required to be passed in for $. Found none'
720
+ 'At least 1 attributes out of ["consistency", "availability", "partitioning"] are required to be passed in for $. Found none'
679
721
  )
680
722
  end
681
723
  end
@@ -1172,4 +1214,121 @@ describe Attributor::Hash do
1172
1214
 
1173
1215
  context Attributor::InvalidDefinition do
1174
1216
  end
1217
+
1218
+
1219
+ context '.as_json_hash' do
1220
+ let(:example){ nil }
1221
+ subject(:description) { type.as_json_schema(example: example) }
1222
+ its([:type]){ should eq(:object)}
1223
+ its([:'x-type_name']){ should eq('Hash')}
1224
+
1225
+ context 'for hashes with explicit key and value types' do
1226
+ let(:key_type){ String }
1227
+ let(:value_type){ Integer }
1228
+
1229
+ subject(:type) { Attributor::Hash.of(key: key_type, value: value_type) }
1230
+
1231
+ it 'describes the key type correctly' do
1232
+ expect(description.keys).to include( :'x-key_type' )
1233
+ expect(description[:'x-key_type']).to be_kind_of(::Hash)
1234
+ expect(description[:'x-key_type'][:type]).to eq( :string )
1235
+ end
1236
+
1237
+ it 'describes the value type correctly' do
1238
+ expect(description.keys).to include( :'x-value_type' )
1239
+ expect(description[:'x-value_type']).to be_kind_of(::Hash)
1240
+ expect(description[:'x-value_type'][:type]).to eq( :integer )
1241
+ end
1242
+
1243
+ end
1244
+
1245
+
1246
+ context 'for hashes with specific keys defined' do
1247
+ let(:block) do
1248
+ proc do
1249
+ key 'a string', String
1250
+ key '1', Integer, min: 1, max: 20
1251
+ key 'some_date', DateTime
1252
+ key 'defaulted', String, default: 'default value'
1253
+ requires do
1254
+ all.of '1','some_date'
1255
+ exclusive 'some_date', 'defaulted'
1256
+ at_least(1).of 'a string', 'some_date'
1257
+ at_most(2).of 'a string', 'some_date'
1258
+ exactly(1).of 'a string', 'some_date'
1259
+ end
1260
+ end
1261
+ end
1262
+
1263
+ let(:type) { Attributor::Hash.of(key: String).construct(block) }
1264
+
1265
+ it 'describes the basic type options correctly' do
1266
+ expect(description[:type]).to eq(:object)
1267
+ expect(description[:'x-key_type']).to eq( type: :string , 'x-type_name': 'String')
1268
+ expect(description).to_not have_key(:'x-value_type')
1269
+ end
1270
+
1271
+ it 'describes the type attributes correctly' do
1272
+ props = description[:properties]
1273
+
1274
+ expect(props['a string']).to eq(type: :string, 'x-type_name': 'String')
1275
+ expect(props['1']).to eq(type: :integer, 'x-type_name': 'Integer', minimum: 1, maximum: 20)
1276
+ expect(props['some_date']).to eq(type: :string, 'x-type_name': 'DateTime', format: :'date-time')
1277
+ expect(props['defaulted']).to eq(type: :string, 'x-type_name': 'String', default: 'default value')
1278
+ end
1279
+
1280
+ it 'describes the attribute requirements correctly' do
1281
+ reqs = description[:required]
1282
+ expect(reqs).to be_kind_of(Array)
1283
+ expect(reqs).to eq( ['1','some_date'] )
1284
+ end
1285
+
1286
+ it 'describes the extended requirements correctly' do
1287
+ reqs = description[:'x-requirements']
1288
+ expect(reqs).to be_kind_of(Array)
1289
+ expect(reqs.size).to be(5)
1290
+ expect(reqs).to include( type: :all, attributes: ['1','some_date'] )
1291
+ expect(reqs).to include( type: :exclusive, attributes: ['some_date','defaulted'] )
1292
+ expect(reqs).to include( type: :at_least, attributes: ['a string','some_date'], count: 1 )
1293
+ expect(reqs).to include( type: :at_most, attributes: ['a string','some_date'], count: 2 )
1294
+ expect(reqs).to include( type: :exactly, attributes: ['a string','some_date'], count: 1 )
1295
+ end
1296
+
1297
+ context 'merging requires.all with attribute required: true' do
1298
+ let(:block) do
1299
+ proc do
1300
+ key 'required string', String, required: true
1301
+ key '1', Integer
1302
+ key 'some_date', DateTime
1303
+ requires do
1304
+ all.of 'some_date'
1305
+ end
1306
+ end
1307
+ end
1308
+ it 'includes attributes with required: true into :required' do
1309
+ expect(description[:required].size).to eq(2)
1310
+ expect(description[:required]).to include( 'required string','some_date' )
1311
+ end
1312
+
1313
+ it 'includes attributes with required: true into the :all requirements' do
1314
+ req_all = description[:'x-requirements'].select{|r| r[:type] == :all}.first
1315
+ expect(req_all[:attributes]).to include( 'required string','some_date' )
1316
+ end
1317
+ end
1318
+
1319
+
1320
+ context 'with an example' do
1321
+ let(:example){ type.example }
1322
+
1323
+ it 'should have the matching example for each leaf key' do
1324
+ expect(description[:properties].keys).to include(*type.keys.keys)
1325
+ description[:properties].each do |name,sub_description|
1326
+ expect(sub_description).to have_key(:example)
1327
+ val = type.attributes[name].dump(example[name])
1328
+ expect(sub_description[:example]).to eq val
1329
+ end
1330
+ end
1331
+ end
1332
+ end
1333
+ end
1175
1334
  end
@@ -146,4 +146,13 @@ describe Attributor::Integer do
146
146
  end
147
147
  end
148
148
  end
149
+
150
+ context '.as_json_schema' do
151
+ subject(:js){ type.as_json_schema }
152
+ it 'adds the right stuff' do
153
+ expect(js.keys).to include(:type, :'x-type_name')
154
+ expect(js[:type]).to eq(:integer)
155
+ expect(js[:'x-type_name']).to eq('Integer')
156
+ end
157
+ end
149
158
  end
@@ -371,7 +371,12 @@ describe Attributor::Model do
371
371
  context 'for models using the "requires" DSL' do
372
372
  subject(:address) { Address.load({state: 'CA'}) }
373
373
  its(:validate) { should_not be_empty }
374
- its(:validate) { should include 'Key name is required for $.' }
374
+ its(:validate) { should include 'Attribute $.key(:name) is required.' }
375
+ end
376
+ context 'for models with non-nullable attributes' do
377
+ subject(:address) { Address.load({name: nil, state: nil}) }
378
+ its(:validate) { should_not be_empty }
379
+ its(:validate) { should include 'Attribute $.state is not nullable.' } # name is nullable
375
380
  end
376
381
  context 'for models with circular sub-attributes' do
377
382
  context 'that are valid' do
@@ -478,6 +483,7 @@ describe Attributor::Model do
478
483
 
479
484
  it 'supports defining sub-attributes using the proper reference' do
480
485
  expect(struct.attributes[:neighbors].options[:required]).to be true
486
+ expect(struct.attributes[:neighbors].options[:null]).to be false
481
487
  expect(struct.attributes[:neighbors].type.member_attribute.type.attributes.keys).to match_array [:name, :age]
482
488
 
483
489
  name_options = struct.attributes[:neighbors].type.member_attribute.type.attributes[:name].options
@@ -64,4 +64,14 @@ describe Attributor::String do
64
64
  end.to raise_error(Attributor::IncompatibleTypeError)
65
65
  end
66
66
  end
67
+
68
+ context '.as_json_schema' do
69
+ subject(:js){ type.as_json_schema(attribute_options: { regexp: /^Foobar$/ }) }
70
+ it 'adds the right attributes' do
71
+ expect(js.keys).to include(:type, :'x-type_name')
72
+ expect(js[:type]).to eq(:string)
73
+ expect(js[:'x-type_name']).to eq('String')
74
+ expect(js[:pattern]).to eq('^Foobar$')
75
+ end
76
+ end
67
77
  end
@@ -1,7 +1,11 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Attributor::Temporal do
4
- subject(:type) { Attributor::Temporal }
4
+ subject(:type) do
5
+ Class.new do
6
+ include Attributor::Temporal
7
+ end
8
+ end
5
9
 
6
10
  it 'raises an exception for native_type' do
7
11
  expect { type.native_type }.to raise_error(NotImplementedError)
@@ -90,4 +90,13 @@ describe Attributor::Time do
90
90
  end
91
91
  end
92
92
  end
93
+ context '.as_json_schema' do
94
+ subject(:js){ type.as_json_schema }
95
+ it 'adds the right attributes' do
96
+ expect(js.keys).to include(:type, :'x-type_name')
97
+ expect(js[:type]).to eq(:string)
98
+ expect(js[:format]).to eq(:'time')
99
+ expect(js[:'x-type_name']).to eq('Time')
100
+ end
101
+ end
93
102
  end
@@ -109,4 +109,13 @@ describe Attributor::URI do
109
109
  end
110
110
  end
111
111
  end
112
+ context '.as_json_schema' do
113
+ subject(:js){ type.as_json_schema }
114
+ it 'adds the right attributes' do
115
+ expect(js.keys).to include(:type, :'x-type_name')
116
+ expect(js[:type]).to eq(:string)
117
+ expect(js[:format]).to eq(:uri)
118
+ expect(js[:'x-type_name']).to eq('URI')
119
+ end
120
+ end
112
121
  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: '5.4'
4
+ version: '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: 2020-05-01 00:00:00.000000000 Z
12
+ date: 2021-11-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: hashie
@@ -312,7 +312,6 @@ files:
312
312
  - attributor.gemspec
313
313
  - lib/attributor.rb
314
314
  - lib/attributor/attribute.rb
315
- - lib/attributor/attribute_resolver.rb
316
315
  - lib/attributor/dsl_compiler.rb
317
316
  - lib/attributor/dumpable.rb
318
317
  - lib/attributor/example_mixin.rb
@@ -350,7 +349,6 @@ files:
350
349
  - lib/attributor/types/time.rb
351
350
  - lib/attributor/types/uri.rb
352
351
  - lib/attributor/version.rb
353
- - spec/attribute_resolver_spec.rb
354
352
  - spec/attribute_spec.rb
355
353
  - spec/attributor_spec.rb
356
354
  - spec/dsl_compiler_spec.rb
@@ -361,6 +359,7 @@ files:
361
359
  - spec/smart_attribute_selector_spec.rb
362
360
  - spec/spec_helper.rb
363
361
  - spec/support/hashes.rb
362
+ - spec/support/integers.rb
364
363
  - spec/support/models.rb
365
364
  - spec/support/polymorphics.rb
366
365
  - spec/type_spec.rb
@@ -406,12 +405,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
406
405
  - !ruby/object:Gem::Version
407
406
  version: '0'
408
407
  requirements: []
409
- rubygems_version: 3.0.3
408
+ rubygems_version: 3.1.2
410
409
  signing_key:
411
410
  specification_version: 4
412
411
  summary: A powerful attribute and type management library for Ruby
413
412
  test_files:
414
- - spec/attribute_resolver_spec.rb
415
413
  - spec/attribute_spec.rb
416
414
  - spec/attributor_spec.rb
417
415
  - spec/dsl_compiler_spec.rb
@@ -422,6 +420,7 @@ test_files:
422
420
  - spec/smart_attribute_selector_spec.rb
423
421
  - spec/spec_helper.rb
424
422
  - spec/support/hashes.rb
423
+ - spec/support/integers.rb
425
424
  - spec/support/models.rb
426
425
  - spec/support/polymorphics.rb
427
426
  - spec/type_spec.rb
@@ -1,111 +0,0 @@
1
- require 'ostruct'
2
-
3
- module Attributor
4
- class AttributeResolver
5
- ROOT_PREFIX = '$'.freeze
6
- COLLECTION_INDEX_KEY = /^at\((\d+)\)$/
7
-
8
- class Data < ::Hash
9
- include Hashie::Extensions::MethodReader
10
- end
11
-
12
- attr_reader :data
13
-
14
- def initialize
15
- @data = Data.new
16
- end
17
-
18
- def query!(key_path, path_prefix = ROOT_PREFIX)
19
- # If the incoming key_path is not an absolute path, append the given prefix
20
- # NOTE: Need to index key_path by range here because Ruby 1.8 returns a
21
- # FixNum for the ASCII code, not the actual character, when indexing by a number.
22
- unless key_path[0..0] == ROOT_PREFIX
23
- # TODO: prepend path_prefix to path_prefix if it did not include it? hm.
24
- key_path = path_prefix + SEPARATOR + key_path
25
- end
26
-
27
- # Discard the initial element, which should always be ROOT_PREFIX at this point
28
- _root, *path = key_path.split(SEPARATOR)
29
-
30
- # Follow the hierarchy path to the requested node and return it:
31
- # Example path => ["instance", "ssh_key", "name"]
32
- # Example @data => {"instance" => { "ssh_key" => { "name" => "foobar" } }}
33
- #
34
- # at(n) is a collection index:
35
- # Example path => ["filters", "at(0)", "type"]
36
- # Example data => {"filters" => [{ "type" => "instance:tag" }]}
37
- #
38
- result = path.inject(@data) do |hash, key|
39
- return nil if hash.nil?
40
- if (match = key.match(COLLECTION_INDEX_KEY))
41
- hash[match[1].to_i]
42
- else
43
- hash.send key
44
- end
45
- end
46
- result
47
- end
48
-
49
- # Query for a certain key in the attribute hierarchy
50
- #
51
- # @param [String] key_path The name of the key to query and its path
52
- # @param [String] path_prefix
53
- #
54
- # @return [String] The value of the specified attribute/key
55
- #
56
- def query(key_path, path_prefix = ROOT_PREFIX)
57
- query!(key_path, path_prefix)
58
- rescue NoMethodError
59
- nil
60
- end
61
-
62
- def register(key_path, value)
63
- if key_path.split(SEPARATOR).size > 1
64
- raise AttributorException, "can only register top-level attributes. got: #{key_path}"
65
- end
66
-
67
- @data[key_path] = value
68
- end
69
-
70
- # Checks that the the condition is met. This means the attribute identified
71
- # by path_prefix and key_path satisfies the optional predicate, which when
72
- # nil simply checks for existence.
73
- #
74
- # @param path_prefix [String]
75
- # @param key_path [String]
76
- # @param predicate [String|Regexp|Proc|NilClass]
77
- #
78
- # @returns [Boolean] True if :required_if condition is met, false otherwise
79
- #
80
- # @raise [AttributorException] When an unsupported predicate is passed
81
- #
82
- def check(path_prefix, key_path, predicate = nil)
83
- value = query(key_path, path_prefix)
84
-
85
- # we have a value, any value, which is good enough given no predicate
86
- return true if !value.nil? && predicate.nil?
87
-
88
- case predicate
89
- when ::String, ::Regexp, ::Integer, ::Float, ::DateTime, true, false
90
- return predicate === value
91
- when ::Proc
92
- # Cannot use === here as above due to different behavior in Ruby 1.8
93
- return predicate.call(value)
94
- when nil
95
- return !value.nil?
96
- else
97
- raise AttributorException, "predicate not supported: #{predicate.inspect}"
98
- end
99
- end
100
-
101
- # TODO: kill this when we also kill Taylor's IdentityMap.current
102
- def self.current=(resolver)
103
- Thread.current[:_attributor_attribute_resolver] = resolver
104
- end
105
-
106
- def self.current
107
- raise AttributorException, 'No AttributeResolver set.' unless Thread.current[:_attributor_attribute_resolver]
108
- Thread.current[:_attributor_attribute_resolver]
109
- end
110
- end
111
- end