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 +4 -4
- data/CHANGELOG.md +7 -0
- data/lib/attributor/attribute.rb +33 -78
- data/lib/attributor/hash_dsl_compiler.rb +6 -5
- data/lib/attributor/types/hash.rb +24 -16
- data/lib/attributor/types/model.rb +9 -21
- data/lib/attributor/version.rb +1 -1
- data/lib/attributor.rb +2 -6
- data/spec/attribute_spec.rb +70 -121
- data/spec/hash_dsl_compiler_spec.rb +5 -5
- data/spec/spec_helper.rb +0 -2
- data/spec/support/models.rb +7 -7
- data/spec/types/hash_spec.rb +63 -22
- data/spec/types/model_spec.rb +7 -1
- metadata +3 -6
- data/lib/attributor/attribute_resolver.rb +0 -111
- data/spec/attribute_resolver_spec.rb +0 -237
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 108abdb1d351f95b78cfae24b61bf8e5bf3943626d8badb691e28ce0df23c45d
|
|
4
|
+
data.tar.gz: 96f4e678e7bccb85526a7a3788aa2df538902200fcb50b49e0de3a36c5d3778d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
data/lib/attributor/attribute.rb
CHANGED
|
@@ -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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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, :
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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 :
|
|
332
|
-
raise AttributorException, '
|
|
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
|
-
|
|
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
|
|
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}
|
|
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}
|
|
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 "
|
|
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
|
-
|
|
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 =
|
|
639
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
data/lib/attributor/version.rb
CHANGED
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
|
-
|
|
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
|
data/spec/attribute_spec.rb
CHANGED
|
@@ -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
|
|
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!(
|
|
76
|
-
h[:options] = {:
|
|
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
|
|
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) { [
|
|
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
|
|
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
|
|
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
|
|
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) { '
|
|
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
|
|
data/spec/support/models.rb
CHANGED
|
@@ -11,9 +11,9 @@ end
|
|
|
11
11
|
|
|
12
12
|
class Duck < Attributor::Model
|
|
13
13
|
attributes do
|
|
14
|
-
attribute :age, Attributor::Integer
|
|
14
|
+
attribute :age, Attributor::Integer
|
|
15
15
|
attribute :name, Attributor::String
|
|
16
|
-
attribute :email, Attributor::String
|
|
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 :
|
|
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
|
data/spec/types/hash_spec.rb
CHANGED
|
@@ -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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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'
|
|
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
|
-
|
|
574
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
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
|
|
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
|
data/spec/types/model_spec.rb
CHANGED
|
@@ -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 '
|
|
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: '
|
|
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-
|
|
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.
|
|
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
|