attributor 5.7 → 6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  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