attributor 5.7 → 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
  SHA256:
3
- metadata.gz: 72e523c77927ca8972951fa08ca3d9171f7601053d2a6d2ead46da6a9a1e2c4b
4
- data.tar.gz: d6e008f822dfd90cb035dfdf66fd52d9fbea8bba35f02073dd2363aa4714ec64
3
+ metadata.gz: 108abdb1d351f95b78cfae24b61bf8e5bf3943626d8badb691e28ce0df23c45d
4
+ data.tar.gz: 96f4e678e7bccb85526a7a3788aa2df538902200fcb50b49e0de3a36c5d3778d
5
5
  SHA512:
6
- metadata.gz: 3404afef9afcc264b5394357c292155faa8ccf231e2f266b8e986d4234a16336a9d85b9e457d2bd16515bd01c95523460f1842dcdbde721d888dd3d80560c2b2
7
- data.tar.gz: 65bc2b01956e42002e0b4a2e6fa54e93edd033dac3dae883225595f186b4f460823ffc82d32d5a6c9eba1a2ee7116325ca3d4af31e27f536462e6fd14fdc68fb
6
+ metadata.gz: 54217688f13fad04f249aa351062afd490dd3962278f7dbdcc5d31e05bd5df3151fc51a704208a1502d8d9cf67a2382ca10b85f331c577610223a39c7b96caa9
7
+ data.tar.gz: 8eb61d8a53f01d2529d1d2f15c77cd9b55a6a0f53f9a9f83db61ee87ae6091761c330ca026759146e05b73b43625fa5f51236e50a98cbee5aa55afe94b6f1c25
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Attributor Changelog
2
2
 
3
+ ## next
4
+
5
+ ## 6.0 (22/11/2021)
6
+ - removed `required_if` support and all of the necessary code.
7
+ - changed the semantics of the `required:` option in attributes, to really mean if the "key" is required to be passed in or not (i.e., check if the key is null, not if its value is null)
8
+ - Introduced a new option`null: true|false` to allow for the value of an attribute to be nullable or not when the attribute is passed in.
9
+ * The default behavior for an attribute nullability currently `null: false` (but it can be easily changed by overriding the `Attributor::Attribute.default_for_null` function to return `true`)
3
10
  ## 5.7 (1/7/2021)
4
11
 
5
12
  - added `custom_option` to Attributor::Attribute class, accepting a name and Attribute arguments that will be used to validate the option value(s) provided.
@@ -40,9 +40,7 @@ module Attributor
40
40
  # @block: code definition for struct attributes (nil for predefined types or leaf/simple types)
41
41
  def initialize(type, options = {}, &block)
42
42
  @type = Attributor.resolve_type(type, options, block)
43
-
44
- @options = options
45
- @options = @type.options.merge(@options) if @type.respond_to?(:options)
43
+ @options = @type.respond_to?(:options) ? @type.options.merge(options) : options
46
44
 
47
45
  check_options!
48
46
  end
@@ -96,18 +94,16 @@ module Attributor
96
94
 
97
95
  def validate_type(value, context)
98
96
  # delegate check to type subclass if it exists
99
- unless type.valid_type?(value)
100
- msg = "Attribute #{Attributor.humanize_context(context)} received value: "
101
- msg += "#{Attributor.errorize_value(value)} is of the wrong type "
102
- msg += "(got: #{value.class.name}, expected: #{type.name})"
103
- return [msg]
104
- end
105
- []
97
+ return [] if value.nil? || type.valid_type?(value)
98
+
99
+ msg = "Attribute #{Attributor.humanize_context(context)} received value: "
100
+ msg += "#{Attributor.errorize_value(value)} is of the wrong type "
101
+ msg += "(got: #{value.class.name}, expected: #{type.name})"
102
+ [msg]
106
103
  end
107
104
 
108
- TOP_LEVEL_OPTIONS = [:description, :values, :default, :example, :required, :required_if, :custom_data].freeze
105
+ TOP_LEVEL_OPTIONS = [:description, :values, :default, :example, :required, :null, :custom_data].freeze
109
106
  INTERNAL_OPTIONS = [:dsl_compiler, :dsl_compiler_options].freeze # Options we don't want to expose when describing attributes
110
- JSON_SCHEMA_UNSUPPORTED_OPTIONS = [ :required, :required_if ].freeze
111
107
  def describe(shallow=true, example: nil)
112
108
  description = { }
113
109
  # Clone the common options
@@ -228,78 +224,38 @@ module Attributor
228
224
  type.attributes if @type_has_attributes ||= type.respond_to?(:attributes)
229
225
  end
230
226
 
227
+ # Default value for a non-specified null: option
228
+ def self.default_for_null
229
+ false
230
+ end
231
+
232
+ # It is only nullable if there is an explicit null: true (or if it's not passed/set, and the default is true)
233
+ def self.nullable_attribute?(options)
234
+ !options.key?(:null) ? default_for_null : options[:null]
235
+ end
236
+
231
237
  # Validates stuff and checks dependencies
232
238
  def validate(object, context = Attributor::DEFAULT_ROOT_CONTEXT)
233
239
  raise "INVALID CONTEXT!! #{context}" unless context
234
240
  # Validate any requirements, absolute or conditional, and return.
235
241
 
236
- if object.nil? # == Attributor::UNSET
237
- # With no value, we can only validate whether that is acceptable or not and return.
238
- # Beyond that, no further validation should be done.
239
- return validate_missing_value(context)
240
- end
241
-
242
- # TODO: support validation for other types of conditional dependencies based on values of other attributes
243
-
244
- errors = validate_type(object, context)
245
-
246
- # End validation if we don't even have the proper type to begin with
247
- return errors if errors.any?
248
-
249
- if options[:values] && !options[:values].include?(object)
250
- errors << "Attribute #{Attributor.humanize_context(context)}: #{Attributor.errorize_value(object)} is not within the allowed values=#{options[:values].inspect} "
251
- end
252
-
253
- errors + type.validate(object, context, self)
254
- end
255
-
256
- def validate_missing_value(context)
257
- raise "INVALID CONTEXT!!! (got: #{context.inspect})" unless context.is_a? Enumerable
258
-
259
- # Missing attribute was required if :required option was set
260
- return ["Attribute #{Attributor.humanize_context(context)} is required"] if options[:required]
261
-
262
- # Missing attribute was not required if :required_if (and :required)
263
- # option was NOT set
264
- requirement = options[:required_if]
265
- return [] unless requirement
266
-
267
- case requirement
268
- when ::String
269
- key_path = requirement
270
- predicate = nil
271
- when ::Hash
272
- # TODO: support multiple dependencies?
273
- key_path = requirement.keys.first
274
- predicate = requirement.values.first
242
+ errors = []
243
+ if object.nil? && !self.class.nullable_attribute?(options)
244
+ errors << "Attribute #{Attributor.humanize_context(context)} is not nullable"
275
245
  else
276
- # should never get here if the option validation worked...
277
- raise AttributorException, "unknown type of dependency: #{requirement.inspect} for #{Attributor.humanize_context(context)}"
278
- end
279
-
280
- # chop off the last part
281
- requirement_context = context[0..-2]
282
- requirement_context_string = requirement_context.join(Attributor::SEPARATOR)
246
+ errors.push *validate_type(object, context)
283
247
 
284
- # FIXME: we're having to reconstruct a string context just to use the resolver...smell.
285
- if AttributeResolver.current.check(requirement_context_string, key_path, predicate)
286
- message = "Attribute #{Attributor.humanize_context(context)} is required when #{key_path} "
287
-
288
- # give a hint about what the full path for a relative key_path would be
289
- unless key_path[0..0] == Attributor::AttributeResolver::ROOT_PREFIX
290
- message << "(for #{Attributor.humanize_context(requirement_context)}) "
248
+ # If the value is null we skip value validation:
249
+ # a) If null wasn't allowed, it would have failed above.
250
+ # b) If null was allowed, we always allow that as a valid value
251
+ if !object.nil? && options[:values] && !options[:values].include?(object)
252
+ errors << "Attribute #{Attributor.humanize_context(context)}: #{Attributor.errorize_value(object)} is not within the allowed values=#{options[:values].inspect} "
291
253
  end
292
-
293
- message << if predicate
294
- "matches #{predicate.inspect}."
295
- else
296
- 'is present.'
297
- end
298
-
299
- [message]
300
- else
301
- []
302
254
  end
255
+
256
+ return errors if errors.any?
257
+
258
+ object.nil? ? errors : errors + type.validate(object, context, self)
303
259
  end
304
260
 
305
261
  def check_options!
@@ -328,9 +284,8 @@ module Attributor
328
284
  when :required
329
285
  raise AttributorException, 'Required must be a boolean' unless definition == true || definition == false
330
286
  raise AttributorException, 'Required cannot be enabled in combination with :default' if definition == true && options.key?(:default)
331
- when :required_if
332
- raise AttributorException, 'Required_if must be a String, a Hash definition or a Proc' unless definition.is_a?(::String) || definition.is_a?(::Hash) || definition.is_a?(::Proc)
333
- raise AttributorException, 'Required_if cannot be specified together with :required' if options[:required]
287
+ when :null
288
+ raise AttributorException, 'Null must be a boolean' unless definition == true || definition == false
334
289
  when :example
335
290
  unless definition.is_a?(::Regexp) || definition.is_a?(::String) || definition.is_a?(::Array) || definition.is_a?(::Proc) || definition.nil? || type.valid_type?(definition)
336
291
  raise AttributorException, "Invalid example type (got: #{definition.class.name}). It must always match the type of the attribute (except if passing Regex that is allowed for some types)"
@@ -35,30 +35,31 @@ module Attributor
35
35
  rest = attr_names - keys
36
36
  unless rest.empty?
37
37
  rest.each do |attr|
38
- result.push "Key #{attr} is required for #{Attributor.humanize_context(context)}."
38
+ sub_context = Attributor::Hash.generate_subcontext(context, attr)
39
+ result.push "Attribute #{Attributor.humanize_context(sub_context)} is required."
39
40
  end
40
41
  end
41
42
  when :exactly
42
43
  included = attr_names & keys
43
44
  unless included.size == number
44
- result.push "Exactly #{number} of the following keys #{attr_names} are required for #{Attributor.humanize_context(context)}. Found #{included.size} instead: #{included.inspect}"
45
+ result.push "Exactly #{number} of the following attributes #{attr_names} are required for #{Attributor.humanize_context(context)}. Found #{included.size} instead: #{included.inspect}"
45
46
  end
46
47
  when :at_most
47
48
  rest = attr_names & keys
48
49
  if rest.size > number
49
50
  found = rest.empty? ? 'none' : rest.inspect
50
- result.push "At most #{number} keys out of #{attr_names} can be passed in for #{Attributor.humanize_context(context)}. Found #{found}"
51
+ result.push "At most #{number} attributes out of #{attr_names} can be passed in for #{Attributor.humanize_context(context)}. Found #{found}"
51
52
  end
52
53
  when :at_least
53
54
  rest = attr_names & keys
54
55
  if rest.size < number
55
56
  found = rest.empty? ? 'none' : rest.inspect
56
- result.push "At least #{number} keys out of #{attr_names} are required to be passed in for #{Attributor.humanize_context(context)}. Found #{found}"
57
+ result.push "At least #{number} attributes out of #{attr_names} are required to be passed in for #{Attributor.humanize_context(context)}. Found #{found}"
57
58
  end
58
59
  when :exclusive
59
60
  intersection = attr_names & keys
60
61
  if intersection.size > 1
61
- result.push "keys #{intersection.inspect} are mutually exclusive for #{Attributor.humanize_context(context)}."
62
+ result.push "Attributes #{intersection.inspect} are mutually exclusive for #{Attributor.humanize_context(context)}."
62
63
  end
63
64
  end
64
65
  result
@@ -426,7 +426,6 @@ module Attributor
426
426
  unless object.is_a?(self)
427
427
  raise ArgumentError, "#{name} can not validate object of type #{object.class.name} for #{Attributor.humanize_context(context)}."
428
428
  end
429
-
430
429
  object.validate(context)
431
430
  end
432
431
 
@@ -514,7 +513,6 @@ module Attributor
514
513
  end
515
514
  end
516
515
  # TODO: minProperties and maxProperties and patternProperties
517
- # TODO: map our required_if (and possible our above requirements 'at_least...' to json schema dependencies)
518
516
  hash
519
517
  end
520
518
 
@@ -613,6 +611,12 @@ module Attributor
613
611
  context = [context] if context.is_a? ::String
614
612
 
615
613
  if self.class.keys.any?
614
+ extra_keys = @contents.keys - self.class.keys.keys
615
+ if extra_keys.any? && !self.class.options[:allow_extra]
616
+ return extra_keys.collect do |k|
617
+ "#{Attributor.humanize_context(context)} can not have key: #{k.inspect}"
618
+ end
619
+ end
616
620
  self.validate_keys(context)
617
621
  else
618
622
  self.validate_generic(context)
@@ -622,30 +626,34 @@ module Attributor
622
626
  end
623
627
 
624
628
  def validate_keys(context)
625
- extra_keys = @contents.keys - self.class.keys.keys
626
- if extra_keys.any? && !self.class.options[:allow_extra]
627
- return extra_keys.collect do |k|
628
- "#{Attributor.humanize_context(context)} can not have key: #{k.inspect}"
629
- end
630
- end
631
-
632
629
  errors = []
633
- keys_with_values = []
630
+ keys_provided = []
634
631
 
635
632
  self.class.keys.each do |key, attribute|
636
633
  sub_context = self.class.generate_subcontext(context, key)
637
634
 
638
- value = @contents[key]
639
- keys_with_values << key unless value.nil?
635
+ value = _get_attr(key)
636
+ keys_provided << key if @contents.key?(key)
640
637
 
641
638
  if value.respond_to?(:validating) # really, it's a thing with sub-attributes
642
639
  next if value.validating
643
640
  end
644
-
645
- errors.concat attribute.validate(value, sub_context)
641
+ # Isn't this handled by the requirements validation? NO! we might want to combine
642
+ if attribute.options[:required] && !@contents.key?(key)
643
+ errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is required."]
644
+ end
645
+ if @contents[key].nil?
646
+ if !Attribute.nullable_attribute?(attribute.options) && @contents.key?(key)
647
+ errors.concat ["Attribute #{Attributor.humanize_context(sub_context)} is not nullable."]
648
+ end
649
+ # No need to validate the attribute further if the key wasn't passed...(or we would get nullable errors etc..cause the attribute has no
650
+ # context if its containing key was even passed (and there might not be a containing key for a top level attribute anyways))
651
+ else
652
+ errors.concat attribute.validate(value, sub_context)
653
+ end
646
654
  end
647
655
  self.class.requirements.each do |requirement|
648
- validation_errors = requirement.validate(keys_with_values, context)
656
+ validation_errors = requirement.validate(keys_provided, context)
649
657
  errors.concat(validation_errors) unless validation_errors.empty?
650
658
  end
651
659
  errors
@@ -661,7 +669,7 @@ module Attributor
661
669
 
662
670
  unless value_type == Attributor::Object
663
671
  sub_context = context + ["value(#{value.inspect})"]
664
- errors.concat value_attribute.validate(value, sub_context)
672
+ errors.concat value_attribute.validate(value, sub_context)
665
673
  end
666
674
  end
667
675
  end
@@ -129,27 +129,9 @@ module Attributor
129
129
  @validating = true
130
130
 
131
131
  context = [context] if context.is_a? ::String
132
- keys_with_values = []
133
- errors = []
134
-
135
- self.class.attributes.each do |sub_attribute_name, sub_attribute|
136
- sub_context = self.class.generate_subcontext(context, sub_attribute_name)
137
-
138
- value = __send__(sub_attribute_name)
139
- keys_with_values << sub_attribute_name unless value.nil?
140
-
141
- if value.respond_to?(:validating) # really, it's a thing with sub-attributes
142
- next if value.validating
143
- end
144
-
145
- errors.concat sub_attribute.validate(value, sub_context)
146
- end
147
- self.class.requirements.each do |req|
148
- validation_errors = req.validate(keys_with_values, context)
149
- errors.concat(validation_errors) unless validation_errors.empty?
150
- end
151
-
152
- errors
132
+ # Use the common, underlying attribute validation of the hash (which will use our _get_attr)
133
+ # to know how to retrieve a value from a model (instead of a hash)
134
+ validate_keys(context)
153
135
  ensure
154
136
  @validating = false
155
137
  end
@@ -198,4 +180,10 @@ module Attributor
198
180
  @dumping = false
199
181
  end
200
182
  end
183
+
184
+ # Override the generic way to get a value from an instance (models need to call the method)
185
+ def _get_attr(k)
186
+ __send__(k)
187
+ end
188
+
201
189
  end
@@ -1,3 +1,3 @@
1
1
  module Attributor
2
- VERSION = '5.7'.freeze
2
+ VERSION = '6.0'.freeze
3
3
  end
data/lib/attributor.rb CHANGED
@@ -13,7 +13,6 @@ module Attributor
13
13
  require_relative 'attributor/type'
14
14
  require_relative 'attributor/dsl_compiler'
15
15
  require_relative 'attributor/hash_dsl_compiler'
16
- require_relative 'attributor/attribute_resolver'
17
16
  require_relative 'attributor/smart_attribute_selector'
18
17
 
19
18
  require_relative 'attributor/example_mixin'
@@ -22,7 +21,8 @@ module Attributor
22
21
 
23
22
  # hierarchical separator string for composing human readable attributes
24
23
  SEPARATOR = '.'.freeze
25
- DEFAULT_ROOT_CONTEXT = ['$'].freeze
24
+ ROOT_PREFIX = '$'.freeze
25
+ DEFAULT_ROOT_CONTEXT = [ROOT_PREFIX].freeze
26
26
 
27
27
  # @param type [Class] The class of the type to resolve
28
28
  #
@@ -56,10 +56,6 @@ module Attributor
56
56
 
57
57
  context = Array(context) if context.is_a? ::String
58
58
 
59
- unless context.is_a? Enumerable
60
- raise "INVALID CONTEXT!!! (got: #{context.inspect})"
61
- end
62
-
63
59
  begin
64
60
  return context.join('.')
65
61
  rescue e
@@ -11,7 +11,7 @@ describe Attributor::Attribute do
11
11
 
12
12
  context 'initialize' do
13
13
  its(:type) { should be type }
14
- its(:options) { should be attribute_options }
14
+ its(:options) { should eq attribute_options }
15
15
 
16
16
  it 'calls check_options!' do
17
17
  expect_any_instance_of(Attributor::Attribute).to receive(:check_options!)
@@ -72,8 +72,8 @@ describe Attributor::Attribute do
72
72
  let(:expected) do
73
73
  h = {type: {name: 'String', id: type.id, family: type.family}}
74
74
  common = attribute_options.select{|k,v| Attributor::Attribute::TOP_LEVEL_OPTIONS.include? k }
75
- h.merge!( common )
76
- h[:options] = {:min => 0 }
75
+ h.merge!(common)
76
+ h[:options] = {min: 0}
77
77
  h
78
78
  end
79
79
 
@@ -458,10 +458,49 @@ describe Attributor::Attribute do
458
458
  context 'applying attribute options' do
459
459
  context ':required' do
460
460
  let(:attribute_options) { { required: true } }
461
+ context 'has no effect on a bare attribute' do
462
+ let(:value) { 'val' }
463
+ it 'it does not error, as we do not know if the parent attribute key was passed in (done at the Hash level)' do
464
+ expect(attribute.validate(value, context)).to be_empty
465
+ end
466
+ end
467
+ end
468
+ context ':null false (non-nullable)' do
469
+ let(:attribute_options) { { null: false } }
461
470
  context 'with a nil value' do
462
471
  let(:value) { nil }
463
472
  it 'returns an error' do
464
- expect(attribute.validate(value, context).first).to eq 'Attribute context is required'
473
+ expect(attribute.validate(value, context).first).to eq 'Attribute context is not nullable'
474
+ end
475
+ end
476
+ end
477
+ context ':null true (nullable)' do
478
+ let(:attribute_options) { { null: true } }
479
+ context 'with a nil value' do
480
+ let(:value) { nil }
481
+ it 'does not error' do
482
+ expect(attribute.validate(value, context)).to be_empty
483
+ end
484
+ end
485
+ end
486
+ context 'defaults to non-nullable if null not defined' do
487
+ let(:attribute_options) { { } }
488
+ context 'with a nil value' do
489
+ let(:value) { nil }
490
+ it 'returns an error' do
491
+ expect(Attributor::Attribute.default_for_null).to be(false)
492
+ expect(attribute.validate(value, context).first).to eq 'Attribute context is not nullable'
493
+ end
494
+ end
495
+ end
496
+
497
+ context 'default can be overrideable with true' do
498
+ let(:attribute_options) { { } }
499
+ context 'with a nil value' do
500
+ let(:value) { nil }
501
+ it 'suceeds' do
502
+ expect(Attributor::Attribute).to receive(:default_for_null).and_return(true)
503
+ expect(attribute.validate(value, context)).to be_empty
465
504
  end
466
505
  end
467
506
  end
@@ -507,6 +546,13 @@ describe Attributor::Attribute do
507
546
  end
508
547
  end
509
548
 
549
+ context 'with a nil value' do
550
+ let(:value) { nil }
551
+ it 'returns no errors' do
552
+ expect(errors).to be_empty
553
+ end
554
+ end
555
+
510
556
  context 'with a value of a value different than the native_type' do
511
557
  let(:value) { 1 }
512
558
 
@@ -516,58 +562,6 @@ describe Attributor::Attribute do
516
562
  end
517
563
  end
518
564
  end
519
-
520
- context '#validate_missing_value' do
521
- let(:key) { '$.instance.ssh_key.name' }
522
- let(:value) { /\w+/.gen }
523
-
524
- let(:attribute_options) { { required_if: key } }
525
-
526
- let(:ssh_key) { double('ssh_key', name: value) }
527
- let(:instance) { double('instance', ssh_key: ssh_key) }
528
-
529
- before { Attributor::AttributeResolver.current.register('instance', instance) }
530
-
531
- let(:attribute_context) { ['$', 'params', 'key_material'] }
532
- subject(:errors) { attribute.validate_missing_value(attribute_context) }
533
-
534
- context 'for a simple dependency without a predicate' do
535
- context 'that is satisfied' do
536
- it { should_not be_empty }
537
- end
538
-
539
- context 'that is missing' do
540
- let(:value) { nil }
541
- it { should be_empty }
542
- end
543
- end
544
-
545
- context 'with a dependency that has a predicate' do
546
- let(:value) { 'default_ssh_key_name' }
547
- # subject(:errors) { attribute.validate_missing_value('') }
548
-
549
- context 'where the target attribute exists, and matches the predicate' do
550
- let(:attribute_options) { { required_if: { key => /default/ } } }
551
-
552
- it { should_not be_empty }
553
-
554
- its(:first) { should match(/Attribute #{Regexp.quote(Attributor.humanize_context(attribute_context))} is required when #{Regexp.quote(key)} matches/) }
555
- end
556
-
557
- context 'where the target attribute exists, but does not match the predicate' do
558
- let(:attribute_options) { { required_if: { key => /other/ } } }
559
-
560
- it { should be_empty }
561
- end
562
-
563
- context 'where the target attribute does not exist' do
564
- let(:attribute_options) { { required_if: { key => /default/ } } }
565
- let(:ssh_key) { double('ssh_key', name: nil) }
566
-
567
- it { should be_empty }
568
- end
569
- end
570
- end
571
565
  end
572
566
 
573
567
  context 'for an attribute for a subclass of Model' do
@@ -636,71 +630,6 @@ describe Attributor::Attribute do
636
630
  end
637
631
  end
638
632
  end
639
-
640
- context '#validate_missing_value' do
641
- let(:type) { Duck }
642
- let(:attribute_name) { nil }
643
- let(:attribute) { Duck.attributes[attribute_name] }
644
-
645
- let(:attribute_context) { ['$', 'duck', attribute_name.to_s] }
646
- subject(:errors) { attribute.validate_missing_value(attribute_context) }
647
-
648
- before do
649
- Attributor::AttributeResolver.current.register('duck', duck)
650
- end
651
-
652
- context 'for a dependency with no predicate' do
653
- let(:attribute_name) { :email }
654
-
655
- let(:duck) do
656
- d = Duck.new
657
- d.age = 1
658
- d.name = 'Donald'
659
- d
660
- end
661
-
662
- context 'where the target attribute exists, and matches the predicate' do
663
- it { should_not be_empty }
664
- its(:first) { should eq 'Attribute $.duck.email is required when name (for $.duck) is present.' }
665
- end
666
- context 'where the target attribute does not exist' do
667
- before do
668
- duck.name = nil
669
- end
670
- it { should be_empty }
671
- end
672
- end
673
-
674
- context 'for a dependency with a predicate' do
675
- let(:attribute_name) { :age }
676
-
677
- let(:duck) do
678
- d = Duck.new
679
- d.name = 'Daffy'
680
- d.email = 'daffy@darkwing.uoregon.edu' # he's a duck,get it?
681
- d
682
- end
683
-
684
- context 'where the target attribute exists, and matches the predicate' do
685
- it { should_not be_empty }
686
- its(:first) { should match(/Attribute #{Regexp.quote('$.duck.age')} is required when name #{Regexp.quote('(for $.duck)')} matches/) }
687
- end
688
-
689
- context 'where the target attribute exists, and does not match the predicate' do
690
- before do
691
- duck.name = 'Donald'
692
- end
693
- it { should be_empty }
694
- end
695
-
696
- context 'where the target attribute does not exist' do
697
- before do
698
- duck.name = nil
699
- end
700
- it { should be_empty }
701
- end
702
- end
703
- end
704
633
  end
705
634
  end
706
635
 
@@ -757,4 +686,24 @@ describe Attributor::Attribute do
757
686
  end
758
687
  end
759
688
  end
689
+
690
+ context '.nullable_attribute?' do
691
+ subject { described_class.nullable_attribute?(options) }
692
+ context 'with null: true option' do
693
+ let(:options) { { null: true } }
694
+ it { should be_truthy }
695
+ end
696
+ context 'with null: false option' do
697
+ let(:options) { { null: false } }
698
+ it { should be_falsey }
699
+ end
700
+ context 'defaults to false without any null option' do
701
+ let(:options) { { } }
702
+ it { should be_falsey }
703
+ end
704
+ context 'defaults to false if null: nil' do
705
+ let(:options) { { null: nil } }
706
+ it { should be_falsey }
707
+ end
708
+ end
760
709
  end
@@ -109,31 +109,31 @@ describe Attributor::HashDSLCompiler do
109
109
  context 'for :all' do
110
110
  let(:arguments) { { all: [:one, :two, :three] } }
111
111
  let(:value) { [:one] }
112
- let(:validation_error) { ['Key two is required for $.', 'Key three is required for $.'] }
112
+ let(:validation_error) { ["Attribute $.key(:two) is required.", "Attribute $.key(:three) is required."] }
113
113
  it { expect(subject).to include(*validation_error) }
114
114
  end
115
115
  context 'for :exactly' do
116
116
  let(:requirement) { req_class.new(exactly: 1).of(:one, :two) }
117
117
  let(:value) { [:one, :two] }
118
- let(:validation_error) { 'Exactly 1 of the following keys [:one, :two] are required for $. Found 2 instead: [:one, :two]' }
118
+ let(:validation_error) { 'Exactly 1 of the following attributes [:one, :two] are required for $. Found 2 instead: [:one, :two]' }
119
119
  it { expect(subject).to include(validation_error) }
120
120
  end
121
121
  context 'for :at_least' do
122
122
  let(:requirement) { req_class.new(at_least: 2).of(:one, :two, :three) }
123
123
  let(:value) { [:one] }
124
- let(:validation_error) { 'At least 2 keys out of [:one, :two, :three] are required to be passed in for $. Found [:one]' }
124
+ let(:validation_error) { 'At least 2 attributes out of [:one, :two, :three] are required to be passed in for $. Found [:one]' }
125
125
  it { expect(subject).to include(validation_error) }
126
126
  end
127
127
  context 'for :at_most' do
128
128
  let(:requirement) { req_class.new(at_most: 1).of(:one, :two, :three) }
129
129
  let(:value) { [:one, :two] }
130
- let(:validation_error) { 'At most 1 keys out of [:one, :two, :three] can be passed in for $. Found [:one, :two]' }
130
+ let(:validation_error) { 'At most 1 attributes out of [:one, :two, :three] can be passed in for $. Found [:one, :two]' }
131
131
  it { expect(subject).to include(validation_error) }
132
132
  end
133
133
  context 'for :exclusive' do
134
134
  let(:arguments) { { exclusive: [:one, :two] } }
135
135
  let(:value) { [:one, :two] }
136
- let(:validation_error) { 'keys [:one, :two] are mutually exclusive for $.' }
136
+ let(:validation_error) { 'Attributes [:one, :two] are mutually exclusive for $.' }
137
137
  it { expect(subject).to include(validation_error) }
138
138
  end
139
139
  end
data/spec/spec_helper.rb CHANGED
@@ -23,9 +23,7 @@ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
23
23
 
24
24
  RSpec.configure do |config|
25
25
  config.around(:each) do |example|
26
- Attributor::AttributeResolver.current = Attributor::AttributeResolver.new
27
26
  example.run
28
- Attributor::AttributeResolver.current = nil
29
27
  end
30
28
  end
31
29
 
@@ -11,9 +11,9 @@ end
11
11
 
12
12
  class Duck < Attributor::Model
13
13
  attributes do
14
- attribute :age, Attributor::Integer, required_if: { 'name' => 'Daffy' }
14
+ attribute :age, Attributor::Integer
15
15
  attribute :name, Attributor::String
16
- attribute :email, Attributor::String, required_if: 'name'
16
+ attribute :email, Attributor::String
17
17
  attribute :angry, Attributor::Boolean, default: true, example: /true|false/, description: 'Angry bird?'
18
18
  attribute :weight, Attributor::Float, example: /\d{1,2}\.\d/, description: 'The weight of the duck'
19
19
  attribute :type, Attributor::Symbol, values: [:duck]
@@ -50,15 +50,15 @@ class Cormorant < Attributor::Model
50
50
  end
51
51
 
52
52
  # This will be a collection of arbitrary Ruby Objects
53
- attribute :fish, Attributor::Collection, description: 'All kinds of fish for feeding the babies'
53
+ attribute :all_the_fish, Attributor::Collection, description: 'All kinds of fish for feeding the babies'
54
54
 
55
55
  # This will be a collection of Cormorants (note, this relationship is circular)
56
- attribute :neighbors, Attributor::Collection.of(Cormorant), description: 'Neighbor cormorants'
56
+ attribute :neighbors, Attributor::Collection.of(Cormorant), member_options: {null: false}, description: 'Neighbor cormorants', null: false
57
57
 
58
58
  # This will be a collection of instances of an anonymous Struct class, each having two well-defined attributes
59
59
 
60
60
  attribute :babies, Attributor::Collection.of(Attributor::Struct), description: 'All the babies', member_options: { identity: :name } do
61
- attribute :name, Attributor::String, example: /[:name]/, description: 'The name of the baby cormorant'
61
+ attribute :name, Attributor::String, example: /[:name]/, description: 'The name of the baby cormorant', required: true
62
62
  attribute :months, Attributor::Integer, default: 0, min: 0, description: 'The age in months of the baby cormorant'
63
63
  attribute :weight, Attributor::Float, example: /\d{1,2}\.\d{3}/, description: 'The weight in kg of the baby cormorant'
64
64
  end
@@ -76,8 +76,8 @@ end
76
76
 
77
77
  class Address < Attributor::Model
78
78
  attributes do
79
- attribute :name, String, example: /\w+/
80
- attribute :state, String, values: %w(OR CA)
79
+ attribute :name, String, example: /\w+/, null: true
80
+ attribute :state, String, values: %w(OR CA), null: false
81
81
  attribute :person, Person, example: proc { |address, context| Person.example(context, address: address) }
82
82
  requires :name
83
83
  end
@@ -514,6 +514,7 @@ describe Attributor::Hash do
514
514
  end
515
515
  end
516
516
  end
517
+
517
518
  context '#validate' do
518
519
  context 'for a key and value typed hash' do
519
520
  let(:key_type) { Integer }
@@ -534,45 +535,85 @@ describe Attributor::Hash do
534
535
  context 'for a hash with defined keys' do
535
536
  let(:block) do
536
537
  proc do
537
- key 'integer', Integer
538
+ key 'integer', Integer, null: false # non-nullable, but not required
538
539
  key 'datetime', DateTime
539
- 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
540
542
  end
541
543
  end
542
544
 
543
545
  let(:type) { Attributor::Hash.construct(block) }
544
-
545
- let(:values) { { 'integer' => 'one', 'datetime' => 'now' } }
546
546
  subject(:hash) { type.new(values) }
547
547
 
548
- it 'validates the keys' do
549
- errors = hash.validate
550
- expect(errors).to have(3).items
551
- 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
552
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
+
553
594
  end
554
595
 
555
596
  context 'with requirements defined' do
556
597
  let(:type) { Attributor::Hash.construct(block) }
557
598
 
558
- context 'using requires' do
599
+ context 'using the requires DSL' do
559
600
  let(:block) do
560
601
  proc do
561
602
  key 'name', String
603
+ key 'nevernull', String, null: false
562
604
  key 'consistency', Attributor::Boolean
563
- key 'availability', Attributor::Boolean
564
- key 'partitioning', Attributor::Boolean
605
+ key 'availability', Attributor::Boolean, null: true
606
+ key 'partitioning', Attributor::Boolean, null: true
565
607
  requires 'consistency', 'availability'
566
- requires.all 'name' # Just to show that it is equivalent to 'requires'
608
+ requires.all 'name'
567
609
  end
568
610
  end
569
611
 
570
612
  it 'complains not all the listed elements are set (false or true)' do
571
- errors = type.new('name' => 'CAP').validate
613
+ errors = type.new('name' => 'CAP', 'consistency' => true, 'nevernull' => nil).validate
572
614
  expect(errors).to have(2).items
573
- %w(consistency availability).each do |name|
574
- expect(errors).to include("Key #{name} is required for $.")
575
- end
615
+ expect(errors).to include('Attribute $.key("nevernull") is not nullable.')
616
+ expect(errors).to include('Attribute $.key("availability") is required.')
576
617
  end
577
618
  end
578
619
 
@@ -591,7 +632,7 @@ describe Attributor::Hash do
591
632
  errors = type.new('name' => 'CAP', 'consistency' => false).validate
592
633
  expect(errors).to have(1).items
593
634
  expect(errors).to include(
594
- '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"]'
595
636
  )
596
637
  end
597
638
  end
@@ -610,7 +651,7 @@ describe Attributor::Hash do
610
651
  it 'complains if more than 2 in the group are set (false or true)' do
611
652
  errors = type.new('name' => 'CAP', 'consistency' => false, 'availability' => true, 'partitioning' => false).validate
612
653
  expect(errors).to have(1).items
613
- 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"]')
614
655
  end
615
656
  end
616
657
 
@@ -628,12 +669,12 @@ describe Attributor::Hash do
628
669
  it 'complains if less than 1 in the group are set (false or true)' do
629
670
  errors = type.new('name' => 'CAP').validate
630
671
  expect(errors).to have(1).items
631
- 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: []')
632
673
  end
633
674
  it 'complains if more than 1 in the group are set (false or true)' do
634
675
  errors = type.new('name' => 'CAP', 'consistency' => false, 'availability' => true).validate
635
676
  expect(errors).to have(1).items
636
- 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"]')
637
678
  end
638
679
  end
639
680
 
@@ -651,7 +692,7 @@ describe Attributor::Hash do
651
692
  it 'complains if two or more in the group are set (false or true)' do
652
693
  errors = type.new('name' => 'CAP', 'consistency' => false, 'availability' => true).validate
653
694
  expect(errors).to have(1).items
654
- expect(errors).to include('keys ["consistency", "availability"] are mutually exclusive for $.')
695
+ expect(errors).to include('Attributes ["consistency", "availability"] are mutually exclusive for $.')
655
696
  end
656
697
  end
657
698
 
@@ -676,7 +717,7 @@ describe Attributor::Hash do
676
717
  errors = type.new('name' => 'CAP').validate
677
718
  expect(errors).to have(1).items
678
719
  expect(errors).to include(
679
- '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'
680
721
  )
681
722
  end
682
723
  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
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.7'
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: 2021-01-07 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
@@ -407,12 +405,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
407
405
  - !ruby/object:Gem::Version
408
406
  version: '0'
409
407
  requirements: []
410
- rubygems_version: 3.0.3
408
+ rubygems_version: 3.1.2
411
409
  signing_key:
412
410
  specification_version: 4
413
411
  summary: A powerful attribute and type management library for Ruby
414
412
  test_files:
415
- - spec/attribute_resolver_spec.rb
416
413
  - spec/attribute_spec.rb
417
414
  - spec/attributor_spec.rb
418
415
  - spec/dsl_compiler_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
@@ -1,237 +0,0 @@
1
- require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
-
3
- describe Attributor::AttributeResolver do
4
- let(:value) { /\w+/.gen }
5
-
6
- context 'registering and querying simple values' do
7
- let(:name) { 'string_value' }
8
- before { subject.register(name, value) }
9
-
10
- it 'works' do
11
- expect(subject.query(name)).to be value
12
- end
13
- end
14
-
15
- context 'querying and registering nested values' do
16
- let(:one) { double(two: value) }
17
- let(:key) { 'one.two' }
18
- before { subject.register('one', one) }
19
-
20
- it 'works' do
21
- expect(subject.query(key)).to be value
22
- end
23
- end
24
-
25
- context 'querying nested values from models' do
26
- let(:instance) { double('instance', ssh_key: ssh_key) }
27
- let(:ssh_key) { double('ssh_key', name: value) }
28
- let(:key) { 'instance.ssh_key.name' }
29
-
30
- before { subject.register('instance', instance) }
31
-
32
- it 'works' do
33
- expect(subject.query('instance')).to be instance
34
- expect(subject.query('instance.ssh_key')).to be ssh_key
35
- expect(subject.query(key)).to be value
36
- end
37
-
38
- context 'with a prefix' do
39
- let(:key) { 'name' }
40
- let(:prefix) { '$.instance.ssh_key' }
41
- let(:value) { 'some_name' }
42
- it 'works' do
43
- expect(subject.query(key, prefix)).to be(value)
44
- end
45
- end
46
- end
47
-
48
- context 'querying values that do not exist' do
49
- context 'for a straight key' do
50
- let(:key) { 'missing' }
51
- it 'returns nil' do
52
- expect(subject.query(key)).to be_nil
53
- end
54
- end
55
- context 'for a nested key' do
56
- let(:key) { 'nested.missing' }
57
- it 'returns nil' do
58
- expect(subject.query(key)).to be_nil
59
- end
60
- end
61
- end
62
-
63
- context 'querying collection indices from models' do
64
- let(:instances) { [instance1, instance2] }
65
- let(:instance1) { double('instance1', ssh_key: ssh_key1) }
66
- let(:instance2) { double('instance2', ssh_key: ssh_key2) }
67
- let(:ssh_key1) { double('ssh_key', name: value) }
68
- let(:ssh_key2) { double('ssh_key', name: 'second') }
69
- let(:args) { [path, prefix].compact }
70
-
71
- before { subject.register('instances', instances) }
72
-
73
- it 'resolves the index to the correct member of the collection' do
74
- expect(subject.query('instances')).to be instances
75
- expect(subject.query('instances.at(1).ssh_key')).to be ssh_key2
76
- expect(subject.query('instances.at(0).ssh_key.name')).to be value
77
- end
78
-
79
- it 'returns nil for index out of range' do
80
- expect(subject.query('instances.at(2)')).to be(nil)
81
- expect(subject.query('instances.at(-1)')).to be(nil)
82
- end
83
-
84
- context 'with a prefix' do
85
- let(:key) { 'name' }
86
- let(:prefix) { '$.instances.at(0).ssh_key' }
87
- let(:value) { 'some_name' }
88
-
89
- it 'resolves the index to the correct member of the collection' do
90
- expect(subject.query(key, prefix)).to be(value)
91
- end
92
- end
93
- end
94
-
95
- context 'checking attribute conditions' do
96
- let(:key) { 'instance.ssh_key.name' }
97
- let(:ssh_key) { double('ssh_key', name: value) }
98
- let(:instance_id) { 123 }
99
- let(:instance) { double('instance', ssh_key: ssh_key, id: instance_id) }
100
-
101
- let(:context) { '$' }
102
-
103
- before { subject.register('instance', instance) }
104
-
105
- let(:present_key) { key }
106
- let(:missing_key) { 'instance.ssh_key.something_else' }
107
-
108
- context 'with no condition' do
109
- let(:condition) { nil }
110
- before { expect(ssh_key).to receive(:something_else).and_return(nil) }
111
- it 'works' do
112
- expect(subject.check(context, present_key, condition)).to be true
113
- expect(subject.check(context, missing_key, condition)).to be false
114
- end
115
- end
116
-
117
- context 'with a string condition' do
118
- let(:passing_condition) { value }
119
- let(:failing_condition) { /\w+/.gen }
120
-
121
- it 'works' do
122
- expect(subject.check(context, key, passing_condition)).to be true
123
- expect(subject.check(context, key, failing_condition)).to be false
124
- end
125
- end
126
-
127
- context 'with a regex condition' do
128
- let(:passing_condition) { /\w+/ }
129
- let(:failing_condition) { /\d+/ }
130
-
131
- it 'works' do
132
- expect(subject.check(context, key, passing_condition)).to be true
133
- expect(subject.check(context, key, failing_condition)).to be false
134
- end
135
- end
136
-
137
- context 'with an integer condition' do
138
- let(:key) { 'instance.id' }
139
- let(:passing_condition) { instance_id }
140
- let(:failing_condition) { /\w+/.gen }
141
-
142
- it 'works' do
143
- expect(subject.check(context, key, passing_condition)).to be true
144
- expect(subject.check(context, key, failing_condition)).to be false
145
- end
146
- end
147
-
148
- skip 'with a hash condition' do
149
- end
150
-
151
- context 'with a proc condition' do
152
- let(:passing_condition) { proc { |test_value| test_value == value } }
153
- let(:failing_condition) { proc { |test_value| test_value != value } }
154
-
155
- it 'works' do
156
- expect(subject.check(context, key, passing_condition)).to eq(true)
157
- expect(subject.check(context, key, failing_condition)).to eq(false)
158
- end
159
- end
160
-
161
- context 'with an unsupported condition type' do
162
- let(:condition) { double('weird condition type') }
163
- it 'raises an error' do
164
- expect { subject.check(context, present_key, condition) }.to raise_error(Attributor::AttributorException)
165
- end
166
- end
167
-
168
- context 'with a condition that asserts something IS nil' do
169
- let(:ssh_key) { double('ssh_key', name: nil) }
170
- it 'can be done using the almighty Proc' do
171
- cond = proc { |value| !value.nil? }
172
- expect(subject.check(context, key, cond)).to be false
173
- end
174
- end
175
-
176
- context 'with a relative path' do
177
- let(:context) { '$.instance.ssh_key' }
178
- let(:key) { 'name' }
179
-
180
- it 'works' do
181
- expect(subject.check(context, key, value)).to be true
182
- end
183
- end
184
- end
185
-
186
- # context 'with context stuff...' do
187
-
188
- # let(:ssh_key) { double("ssh_key", name:value) }
189
- # let(:instance) { double("instance", ssh_key:ssh_key) }
190
-
191
- # let(:key) { "ssh_key.name" }
192
- # let(:key) { "$.payload" }
193
- # let(:key) { "ssh_key.name" } # no $ == current object
194
- # let(:key) { "@.ssh_key" } # @ is current object
195
-
196
- # before { subject.register('instance', instance) }
197
-
198
- # it 'works?' do
199
- # # check dependency for 'instance'
200
- # resolver.with 'instance' do |res|
201
- # res.check(key)
202
- # '$.payload'
203
- # end
204
-
205
- # end
206
-
207
- # end
208
-
209
- # context 'integration with attributes that have sub-attributes' do
210
- # when you start to parse... do you set the root in the resolver?
211
- # end
212
- #
213
- # context 'actually using the thing' do
214
-
215
- # # we'll always want to add... right? never really remove?
216
- # # at least not remove for the duration of a given resolver...
217
- # # which will last for one request.
218
- # #
219
- # # could the resolver be an identity-map of sorts for the request?
220
- # # how much overlap is there in there?
221
- # #
222
- # #
223
-
224
- # it 'is really actually quite useful' do
225
- # #attribute = Attributor::Attribute.new ::String, required_if: { "instance.ssh_key.name" : Proc.new { |value| value.nil? } }
226
-
227
- # resolver = Attributor::AttributeResolver.new
228
-
229
- # resolver.register '$.parsed_params', parsed_params
230
- # resolver.register '$.payload', payload
231
-
232
- # resolver.query '$.parsed_params.account_id'
233
-
234
- # end
235
-
236
- # end
237
- end