attributor 4.1.0 → 4.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/lib/attributor.rb +1 -0
- data/lib/attributor/hash_dsl_compiler.rb +141 -0
- data/lib/attributor/type.rb +15 -0
- data/lib/attributor/types/hash.rb +39 -5
- data/lib/attributor/types/model.rb +1 -0
- data/lib/attributor/version.rb +1 -1
- data/spec/hash_dsl_compiler_spec.rb +177 -0
- data/spec/type_spec.rb +30 -0
- data/spec/types/hash_spec.rb +205 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91fa7d928579dfcb275a39160a5a97b3e2ed3714
|
4
|
+
data.tar.gz: f2e689081dfe7e08ff69dd6d5d497fbf9fbadf64
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb003537a99b92977798f35b5b8655e6baaec686e9beb03bb7093413c6fcc99b2bfe00587532ebc8957c30ff9a8de04311bcb45b9be7fdfaa038b9206f6e2701
|
7
|
+
data.tar.gz: 6ae7ade75b1cf99da3a1c317e26e758f92657617f158c47d4778471bee2b5d2e14e623dd111539994404263e4dc2c9bc19b7490af21ad84adba926d114157391
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,35 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 4.2.0
|
6
|
+
|
7
|
+
* Added an "anonymous" DSL for base `Attributor::Type` which is reported in its `.describe` call.
|
8
|
+
* This is a simple documentation bit, that might help the clients to document the type properly (i.e. treat it as if the type was anonymously defined whenever is used, rather than reachable by id/name from anywhere)
|
9
|
+
|
10
|
+
* Built advanced attribute requirements for `Struct`,`Model` and `Hash` types. Those requirements allow you to define things like:
|
11
|
+
* A list of attributes that are required (equivalent to defining the required: true bit at each of the attributes)
|
12
|
+
* At most (n) attributes from a group can be passed in
|
13
|
+
* At least (n) attributes from a group are required
|
14
|
+
* Exactly (n) attributes from a group are required
|
15
|
+
* Example:
|
16
|
+
```
|
17
|
+
requires ‘id’, ‘name’
|
18
|
+
requires.all ‘id’, ‘name’ # Equivalent to above
|
19
|
+
requires.all.of ‘id’, ‘name’ # Equivalent to above again
|
20
|
+
requires.at_most(2).of 'consistency', 'availability', 'partitioning'
|
21
|
+
requires.at_least(1).of ‘rock’, ‘pop’
|
22
|
+
requires.exactly(2).of ‘one’, ‘two’, ’three’
|
23
|
+
```
|
24
|
+
* Same example expressed inside a block if so desired
|
25
|
+
```
|
26
|
+
requires do
|
27
|
+
all 'id', 'name
|
28
|
+
all.of 'id', 'name # Equivalent
|
29
|
+
at_most(2).of 'consistency', 'availability', 'partitioning'
|
30
|
+
…
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
5
34
|
## 4.1.0
|
6
35
|
|
7
36
|
* Added a `Class` type (useful to avoid demodulization coercions etc...)
|
data/lib/attributor.rb
CHANGED
@@ -11,6 +11,7 @@ module Attributor
|
|
11
11
|
require_relative 'attributor/attribute'
|
12
12
|
require_relative 'attributor/type'
|
13
13
|
require_relative 'attributor/dsl_compiler'
|
14
|
+
require_relative 'attributor/hash_dsl_compiler'
|
14
15
|
require_relative 'attributor/attribute_resolver'
|
15
16
|
|
16
17
|
require_relative 'attributor/example_mixin'
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require_relative 'dsl_compiler'
|
2
|
+
|
3
|
+
|
4
|
+
module Attributor
|
5
|
+
|
6
|
+
class HashDSLCompiler < DSLCompiler
|
7
|
+
|
8
|
+
# A class that encapsulates the definition of a requirement for Hash attributes
|
9
|
+
# It implements the validation against incoming values and it describes its format for documentation purposes
|
10
|
+
class Requirement
|
11
|
+
attr_reader :type
|
12
|
+
attr_reader :number
|
13
|
+
attr_reader :attr_names
|
14
|
+
attr_reader :description
|
15
|
+
|
16
|
+
def initialize(description: nil, **spec)
|
17
|
+
@description = description
|
18
|
+
@type = spec.keys.first
|
19
|
+
case type
|
20
|
+
when :all
|
21
|
+
self.of(*spec[type])
|
22
|
+
when :exclusive
|
23
|
+
self.of(*spec[type])
|
24
|
+
else
|
25
|
+
@number = spec[type]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
def of( *args)
|
29
|
+
@attr_names = args
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate( object,context=Attributor::DEFAULT_ROOT_CONTEXT,_attribute)
|
34
|
+
result = []
|
35
|
+
case type
|
36
|
+
when :all
|
37
|
+
rest = attr_names - object.keys
|
38
|
+
unless rest.empty?
|
39
|
+
rest.each do |attr|
|
40
|
+
result.push "Key #{attr} is required for #{Attributor.humanize_context(context)}."
|
41
|
+
end
|
42
|
+
end
|
43
|
+
when :exactly
|
44
|
+
included = attr_names & object.keys
|
45
|
+
unless included.size == number
|
46
|
+
result.push "Exactly #{number} of the following keys #{attr_names} are required for #{Attributor.humanize_context(context)}. Found #{included.size} instead: #{included.inspect}"
|
47
|
+
end
|
48
|
+
when :at_most
|
49
|
+
rest = attr_names & object.keys
|
50
|
+
if rest.size > number
|
51
|
+
found = rest.empty? ? "none" : rest.inspect
|
52
|
+
result.push "At most #{number} keys out of #{attr_names} can be passed in for #{Attributor.humanize_context(context)}. Found #{found}"
|
53
|
+
end
|
54
|
+
when :at_least
|
55
|
+
rest = attr_names & object.keys
|
56
|
+
if rest.size < number
|
57
|
+
found = rest.empty? ? "none" : rest.inspect
|
58
|
+
result.push "At least #{number} keys out of #{attr_names} are required to be passed in for #{Attributor.humanize_context(context)}. Found #{found}"
|
59
|
+
end
|
60
|
+
when :exclusive
|
61
|
+
intersection = attr_names & object.keys
|
62
|
+
if intersection.size > 1
|
63
|
+
result.push "keys #{intersection.inspect} are mutually exclusive for #{Attributor.humanize_context(context)}."
|
64
|
+
end
|
65
|
+
end
|
66
|
+
result
|
67
|
+
end
|
68
|
+
|
69
|
+
def describe(shallow=false, example: nil)
|
70
|
+
hash = {type: type, attributes: attr_names}
|
71
|
+
hash[:count] = number unless number.nil?
|
72
|
+
hash[:description] = description unless description.nil?
|
73
|
+
hash
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
# A class that encapsulates the available DSL under the `requires` keyword.
|
79
|
+
# In particular it allows to define requirements like:
|
80
|
+
# requires.all :attr1, :attr2, :attr3
|
81
|
+
# requires.exclusive :attr1, :attr2, :attr3
|
82
|
+
# requires.at_most(2).of :attr1, :attr2, :attr3
|
83
|
+
# requires.at_least(2).of :attr1, :attr2, :attr3
|
84
|
+
# requires.exactly(2).of :attr1, :attr2, :attr3
|
85
|
+
# Note: all and exclusive can also use .of , it is equivalent
|
86
|
+
class RequiresDSL
|
87
|
+
attr_accessor :target
|
88
|
+
attr_accessor :options
|
89
|
+
def initialize(target, **opts)
|
90
|
+
self.target = target
|
91
|
+
self.options = opts
|
92
|
+
end
|
93
|
+
def all(*attr_names, **opts)
|
94
|
+
req = Requirement.new( options.merge(opts).merge(all: attr_names) )
|
95
|
+
target.add_requirement req
|
96
|
+
req
|
97
|
+
end
|
98
|
+
def at_most(number)
|
99
|
+
req = Requirement.new( options.merge(at_most: number) )
|
100
|
+
target.add_requirement req
|
101
|
+
req
|
102
|
+
end
|
103
|
+
def at_least(number)
|
104
|
+
req = Requirement.new( options.merge(at_least: number) )
|
105
|
+
target.add_requirement req
|
106
|
+
req
|
107
|
+
end
|
108
|
+
def exactly(number)
|
109
|
+
req = Requirement.new( options.merge(exactly: number) )
|
110
|
+
target.add_requirement req
|
111
|
+
req
|
112
|
+
end
|
113
|
+
def exclusive(*attr_names, **opts)
|
114
|
+
req = Requirement.new( options.merge(opts).merge(exclusive: attr_names) )
|
115
|
+
target.add_requirement req
|
116
|
+
req
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def _requirements_dsl
|
121
|
+
@requirements_dsl ||= RequiresDSL.new(@target)
|
122
|
+
end
|
123
|
+
|
124
|
+
def requires(*spec,**opts,&block)
|
125
|
+
if spec.empty?
|
126
|
+
unless opts.empty?
|
127
|
+
self._requirements_dsl.options.merge(opts)
|
128
|
+
end
|
129
|
+
if block_given?
|
130
|
+
self._requirements_dsl.instance_eval(&block)
|
131
|
+
else
|
132
|
+
self._requirements_dsl
|
133
|
+
end
|
134
|
+
else
|
135
|
+
self._requirements_dsl.all(*spec,opts)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
data/lib/attributor/type.rb
CHANGED
@@ -15,6 +15,20 @@ module Attributor
|
|
15
15
|
false
|
16
16
|
end
|
17
17
|
|
18
|
+
# Allow a type to be marked as if it was anonymous (i.e. not referenceable by name)
|
19
|
+
def anonymous_type(val=true)
|
20
|
+
@_anonymous = val
|
21
|
+
end
|
22
|
+
|
23
|
+
def anonymous?
|
24
|
+
if @_anonymous == nil
|
25
|
+
self.name == nil # if nothing is set, consider it anonymous if the class does not have a name
|
26
|
+
else
|
27
|
+
@_anonymous
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
18
32
|
# Generic decoding and coercion of the attribute.
|
19
33
|
def load(value,context=Attributor::DEFAULT_ROOT_CONTEXT, **options)
|
20
34
|
return nil if value.nil?
|
@@ -104,6 +118,7 @@ module Attributor
|
|
104
118
|
family: self.family,
|
105
119
|
id: self.id
|
106
120
|
}
|
121
|
+
hash[:anonymous] = @_anonymous unless @_anonymous.nil?
|
107
122
|
hash[:example] = example if example
|
108
123
|
hash
|
109
124
|
end
|
@@ -29,6 +29,7 @@ module Attributor
|
|
29
29
|
attr_reader :key_attribute
|
30
30
|
attr_reader :insensitive_map
|
31
31
|
attr_accessor :extra_keys
|
32
|
+
attr_reader :requirements
|
32
33
|
end
|
33
34
|
|
34
35
|
@key_type = Object
|
@@ -38,7 +39,7 @@ module Attributor
|
|
38
39
|
@value_attribute = Attribute.new(@value_type)
|
39
40
|
|
40
41
|
@error = false
|
41
|
-
|
42
|
+
@requirements = []
|
42
43
|
|
43
44
|
def self.key_type=(key_type)
|
44
45
|
@key_type = Attributor.resolve_type(key_type)
|
@@ -72,6 +73,7 @@ module Attributor
|
|
72
73
|
@value_type = v
|
73
74
|
@key_attribute = Attribute.new(@key_type)
|
74
75
|
@value_attribute = Attribute.new(@value_type)
|
76
|
+
@requirements = []
|
75
77
|
|
76
78
|
@error = false
|
77
79
|
end
|
@@ -116,7 +118,7 @@ module Attributor
|
|
116
118
|
end
|
117
119
|
|
118
120
|
def self.dsl_class
|
119
|
-
@options[:dsl_compiler] ||
|
121
|
+
@options[:dsl_compiler] || HashDSLCompiler
|
120
122
|
end
|
121
123
|
|
122
124
|
def self.native_type
|
@@ -140,6 +142,16 @@ module Attributor
|
|
140
142
|
true
|
141
143
|
end
|
142
144
|
|
145
|
+
def self.add_requirement(req)
|
146
|
+
@requirements << req
|
147
|
+
return unless req.attr_names
|
148
|
+
non_existing = req.attr_names - self.attributes.keys
|
149
|
+
unless non_existing.empty?
|
150
|
+
raise "Invalid attribute name(s) found (#{non_existing.join(', ')}) when defining a requirement of type #{req.type} for #{Attributor.type_name(self)} ." +
|
151
|
+
"The only existing attributes are #{self.attributes.keys}"
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
143
155
|
|
144
156
|
def self.construct(constructor_block, **options)
|
145
157
|
return self if constructor_block.nil?
|
@@ -427,11 +439,26 @@ module Attributor
|
|
427
439
|
if self.keys.any?
|
428
440
|
# Spit keys if it's the root or if it's an anonymous structures
|
429
441
|
if ( !shallow || self.name == nil)
|
430
|
-
|
442
|
+
required_names = []
|
443
|
+
# FIXME: change to :keys when the praxis doc browser supports displaying those
|
431
444
|
hash[:attributes] = self.keys.each_with_object({}) do |(sub_name, sub_attribute), sub_attributes|
|
445
|
+
required_names << sub_name if sub_attribute.options[:required] == true
|
432
446
|
sub_example = example.get(sub_name) if example
|
433
447
|
sub_attributes[sub_name] = sub_attribute.describe(true, example: sub_example)
|
434
448
|
end
|
449
|
+
hash[:requirements] = self.requirements.each_with_object([]) do |req, list|
|
450
|
+
described_req = req.describe(shallow)
|
451
|
+
if described_req[:type] == :all
|
452
|
+
# Add the names of the attributes that have the required flag too
|
453
|
+
described_req[:attributes] |= required_names
|
454
|
+
required_names = []
|
455
|
+
end
|
456
|
+
list << described_req
|
457
|
+
end
|
458
|
+
# Make sure we create an :all requirement, if there wasn't one so we can add the required: true attributes
|
459
|
+
unless required_names.empty?
|
460
|
+
hash[:requirements] << {type: :all, attributes: required_names }
|
461
|
+
end
|
435
462
|
end
|
436
463
|
else
|
437
464
|
hash[:value] = {type: value_type.describe(true)}
|
@@ -538,7 +565,7 @@ module Attributor
|
|
538
565
|
end
|
539
566
|
end
|
540
567
|
|
541
|
-
self.class.keys.each_with_object(Array.new) do |(key, attribute), errors|
|
568
|
+
ret = self.class.keys.each_with_object(Array.new) do |(key, attribute), errors|
|
542
569
|
sub_context = self.class.generate_subcontext(context,key)
|
543
570
|
|
544
571
|
value = @contents[key]
|
@@ -550,7 +577,7 @@ module Attributor
|
|
550
577
|
errors.push *attribute.validate(value, sub_context)
|
551
578
|
end
|
552
579
|
else
|
553
|
-
@contents.each_with_object(Array.new) do |(key, value), errors|
|
580
|
+
ret = @contents.each_with_object(Array.new) do |(key, value), errors|
|
554
581
|
# FIXME: the sub contexts and error messages don't really make sense here
|
555
582
|
unless key_type == Attributor::Object
|
556
583
|
sub_context = context + ["key(#{key.inspect})"]
|
@@ -563,6 +590,13 @@ module Attributor
|
|
563
590
|
end
|
564
591
|
end
|
565
592
|
end
|
593
|
+
unless self.class.requirements.empty?
|
594
|
+
self.class.requirements.each_with_object(ret) do |req, errors|
|
595
|
+
validation_errors = req.validate( @contents , context)
|
596
|
+
errors.push *validation_errors unless validation_errors.empty?
|
597
|
+
end
|
598
|
+
end
|
599
|
+
ret
|
566
600
|
end
|
567
601
|
|
568
602
|
|
data/lib/attributor/version.rb
CHANGED
@@ -0,0 +1,177 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper.rb')
|
2
|
+
|
3
|
+
|
4
|
+
describe Attributor::HashDSLCompiler do
|
5
|
+
|
6
|
+
let(:target) { double("model", attributes: {}) }
|
7
|
+
|
8
|
+
let(:dsl_compiler_options) { {} }
|
9
|
+
subject(:dsl_compiler) { Attributor::HashDSLCompiler.new(target, dsl_compiler_options) }
|
10
|
+
|
11
|
+
it 'returns the requirements DSL attached to the right target' do
|
12
|
+
req_dsl = dsl_compiler._requirements_dsl
|
13
|
+
req_dsl.should be_kind_of( Attributor::HashDSLCompiler::RequiresDSL )
|
14
|
+
req_dsl.target.should be(target)
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'requires' do
|
18
|
+
|
19
|
+
context 'without any arguments' do
|
20
|
+
it 'without params returns the underlying compiler to chain internal methods' do
|
21
|
+
subject.requires.should be_kind_of( Attributor::HashDSLCompiler::RequiresDSL )
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with params only (and some options)' do
|
26
|
+
it 'takes then array to mean all attributes are required' do
|
27
|
+
target.should_receive(:add_requirement)
|
28
|
+
requirement = subject.requires :one, :two , description: "These are very required"
|
29
|
+
requirement.should be_kind_of( Attributor::HashDSLCompiler::Requirement )
|
30
|
+
requirement.type.should be(:all)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
context 'with a block only (and some options)' do
|
34
|
+
it 'evals it in the context of the Compiler' do
|
35
|
+
proc = Proc.new {}
|
36
|
+
dsl = dsl_compiler._requirements_dsl
|
37
|
+
dsl.should_receive(:instance_eval)#.with(&proc) << Does rspec 2.99 support block args?
|
38
|
+
subject.requires description: "These are very required", &proc
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'RequiresDSL' do
|
46
|
+
|
47
|
+
subject(:dsl){ Attributor::HashDSLCompiler::RequiresDSL.new(target) }
|
48
|
+
it 'stores the received target' do
|
49
|
+
subject.target.should be(target)
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'has DSL methods' do
|
53
|
+
let(:req){ double("requirement") }
|
54
|
+
let(:attr_names){ [:one, :two, :tree] }
|
55
|
+
let(:number){ 2 }
|
56
|
+
let(:req_class){ Attributor::HashDSLCompiler::Requirement }
|
57
|
+
before do
|
58
|
+
target.should_receive(:add_requirement).with(req)
|
59
|
+
end
|
60
|
+
it 'responds to .all' do
|
61
|
+
req_class.should_receive(:new).with( all: attr_names ).and_return(req)
|
62
|
+
subject.all(*attr_names)
|
63
|
+
end
|
64
|
+
it 'responds to .at_most(n)' do
|
65
|
+
req_class.should_receive(:new).with( at_most: number ).and_return(req)
|
66
|
+
subject.at_most(number)
|
67
|
+
end
|
68
|
+
it 'responds to .at_least(n)' do
|
69
|
+
req_class.should_receive(:new).with( at_least: number ).and_return(req)
|
70
|
+
subject.at_least(number)
|
71
|
+
end
|
72
|
+
it 'responds to .exactly(n)' do
|
73
|
+
req_class.should_receive(:new).with( exactly: number ).and_return(req)
|
74
|
+
subject.exactly(number)
|
75
|
+
end
|
76
|
+
it 'responds to .exclusive' do
|
77
|
+
req_class.should_receive(:new).with( exclusive: attr_names ).and_return(req)
|
78
|
+
subject.exclusive(*attr_names)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'Requirement' do
|
85
|
+
|
86
|
+
let(:attr_names){ [:one, :two, :tree] }
|
87
|
+
let(:req_class){ Attributor::HashDSLCompiler::Requirement }
|
88
|
+
|
89
|
+
context 'initialization' do
|
90
|
+
it 'calls .of for exclusive' do
|
91
|
+
req_class.any_instance.should_receive(:of).with(*attr_names)
|
92
|
+
req_class.new(exclusive: attr_names)
|
93
|
+
end
|
94
|
+
it 'calls .of for all' do
|
95
|
+
req_class.any_instance.should_receive(:of).with(*attr_names)
|
96
|
+
req_class.new(all: attr_names)
|
97
|
+
end
|
98
|
+
it 'saves the number for the rest' do
|
99
|
+
req_class.new(exactly: 1).number.should be(1)
|
100
|
+
req_class.new(exactly: 1).type.should be(:exactly)
|
101
|
+
req_class.new(at_most: 2).number.should be(2)
|
102
|
+
req_class.new(at_most: 2).type.should be(:at_most)
|
103
|
+
req_class.new(at_least: 3).number.should be(3)
|
104
|
+
req_class.new(at_least: 3).type.should be(:at_least)
|
105
|
+
end
|
106
|
+
it 'understands and saves a :description' do
|
107
|
+
req = req_class.new(exactly: 1, description: "Hello")
|
108
|
+
req.number.should be(1)
|
109
|
+
req.description.should eq("Hello")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'Requirement#validate' do
|
114
|
+
let(:requirement){ req_class.new(arguments) }
|
115
|
+
let(:subject){ requirement.validate(value,["$"],nil)}
|
116
|
+
|
117
|
+
context 'for :all' do
|
118
|
+
let(:arguments){ { all: [:one, :two, :three] } }
|
119
|
+
let(:value){ {one: 1}}
|
120
|
+
let(:validation_error){ ["Key two is required for $.", "Key three is required for $."] }
|
121
|
+
it { subject.should include(*validation_error) }
|
122
|
+
end
|
123
|
+
context 'for :exactly' do
|
124
|
+
let(:requirement) { req_class.new(exactly: 1).of(:one,:two) }
|
125
|
+
let(:value){ {one: 1, two: 2}}
|
126
|
+
let(:validation_error){ "Exactly 1 of the following keys [:one, :two] are required for $. Found 2 instead: [:one, :two]" }
|
127
|
+
it { subject.should include(validation_error) }
|
128
|
+
end
|
129
|
+
context 'for :at_least' do
|
130
|
+
let(:requirement) { req_class.new(at_least: 2).of(:one,:two,:three) }
|
131
|
+
let(:value){ {one: 1}}
|
132
|
+
let(:validation_error){ "At least 2 keys out of [:one, :two, :three] are required to be passed in for $. Found [:one]" }
|
133
|
+
it { subject.should include(validation_error) }
|
134
|
+
end
|
135
|
+
context 'for :at_most' do
|
136
|
+
let(:requirement) { req_class.new(at_most: 1).of(:one,:two,:three) }
|
137
|
+
let(:value){ {one: 1, two: 2}}
|
138
|
+
let(:validation_error){ "At most 1 keys out of [:one, :two, :three] can be passed in for $. Found [:one, :two]" }
|
139
|
+
it { subject.should include(validation_error) }
|
140
|
+
end
|
141
|
+
context 'for :exclusive' do
|
142
|
+
let(:arguments){ { exclusive: [:one, :two] } }
|
143
|
+
let(:value){ {one: 1, two: 2}}
|
144
|
+
let(:validation_error){ "keys [:one, :two] are mutually exclusive for $." }
|
145
|
+
it { subject.should include(validation_error) }
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'Requirement#describe' do
|
150
|
+
|
151
|
+
it 'should work for :all' do
|
152
|
+
req = req_class.new(all: attr_names).describe
|
153
|
+
req.should eq( type: :all, attributes: [:one, :two, :tree] )
|
154
|
+
end
|
155
|
+
it 'should work for :exclusive n' do
|
156
|
+
req = req_class.new(exclusive: attr_names).describe
|
157
|
+
req.should eq( type: :exclusive, attributes: [:one, :two, :tree] )
|
158
|
+
end
|
159
|
+
it 'should work for :exactly' do
|
160
|
+
req = req_class.new(exactly: 1).of(*attr_names).describe
|
161
|
+
req.should include( type: :exactly, count: 1, attributes: [:one, :two, :tree] )
|
162
|
+
end
|
163
|
+
it 'should work for :at_most n' do
|
164
|
+
req = req_class.new(at_most: 1).of(*attr_names).describe
|
165
|
+
req.should include( type: :at_most, count: 1, attributes: [:one, :two, :tree] )
|
166
|
+
end
|
167
|
+
it 'should work for :at_least n' do
|
168
|
+
req = req_class.new(at_least: 1).of(*attr_names).describe
|
169
|
+
req.should include( type: :at_least, count: 1, attributes: [:one, :two, :tree] )
|
170
|
+
end
|
171
|
+
it 'should report a description' do
|
172
|
+
req = req_class.new(at_least: 1, description: "no more than 1").of(*attr_names).describe
|
173
|
+
req.should include( type: :at_least, count: 1, attributes: [:one, :two, :tree], description: "no more than 1" )
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
data/spec/type_spec.rb
CHANGED
@@ -33,6 +33,22 @@ describe Attributor::Type do
|
|
33
33
|
its(:native_type) { should be(::String) }
|
34
34
|
its(:id) { should eq('Testing')}
|
35
35
|
|
36
|
+
context 'anonymous' do
|
37
|
+
its(:anonymous?) { should be(false) }
|
38
|
+
it 'is true for nameless-types' do
|
39
|
+
klass = Class.new do
|
40
|
+
include Attributor::Type
|
41
|
+
end
|
42
|
+
expect( klass.anonymous? ).to be(true)
|
43
|
+
end
|
44
|
+
it 'can be set to true explicitly' do
|
45
|
+
klass = Class.new(test_type) do
|
46
|
+
anonymous_type
|
47
|
+
end
|
48
|
+
expect( klass.anonymous? ).to be(true)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
36
52
|
context 'load' do
|
37
53
|
let(:value) { nil }
|
38
54
|
let(:context) { nil }
|
@@ -166,6 +182,20 @@ describe Attributor::Type do
|
|
166
182
|
end
|
167
183
|
end
|
168
184
|
|
185
|
+
context 'when anonymous' do
|
186
|
+
|
187
|
+
it 'reports true in the output when set (to true default)' do
|
188
|
+
anon_type = Class.new(test_type) { anonymous_type }
|
189
|
+
anon_type.describe.should have_key(:anonymous)
|
190
|
+
anon_type.describe[:anonymous].should be(true)
|
191
|
+
end
|
192
|
+
it 'reports false in the output when set false explicitly' do
|
193
|
+
anon_type = Class.new(test_type) { anonymous_type false }
|
194
|
+
anon_type.describe.should have_key(:anonymous)
|
195
|
+
anon_type.describe[:anonymous].should be(false)
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
169
199
|
end
|
170
200
|
|
171
201
|
end
|
data/spec/types/hash_spec.rb
CHANGED
@@ -8,8 +8,9 @@ describe Attributor::Hash do
|
|
8
8
|
its(:native_type) { should be(type) }
|
9
9
|
its(:key_type) { should be(Attributor::Object) }
|
10
10
|
its(:value_type) { should be(Attributor::Object) }
|
11
|
+
its(:dsl_class) { should be(Attributor::HashDSLCompiler) }
|
11
12
|
|
12
|
-
context 'attributes' do
|
13
|
+
context 'attributes' do
|
13
14
|
context 'with an exception from the definition block' do
|
14
15
|
subject(:broken_model) do
|
15
16
|
Class.new(Attributor::Model) do
|
@@ -413,6 +414,26 @@ context 'attributes' do
|
|
413
414
|
|
414
415
|
end
|
415
416
|
|
417
|
+
context '.add_requirement' do
|
418
|
+
let(:req_type){ :all }
|
419
|
+
let(:req){ double("requirement", type: req_type, attr_names: req_attributes)}
|
420
|
+
context 'with valid attributes' do
|
421
|
+
let(:req_attributes){ [:name] }
|
422
|
+
it 'successfully saves it in the class' do
|
423
|
+
HashWithStrings.add_requirement(req)
|
424
|
+
HashWithStrings.requirements.should include(req)
|
425
|
+
end
|
426
|
+
end
|
427
|
+
context 'with attributes not defined in the class' do
|
428
|
+
let(:req_attributes){ [:name, :invalid, :notgood] }
|
429
|
+
it 'it complains loudly' do
|
430
|
+
expect{
|
431
|
+
HashWithStrings.add_requirement(req)
|
432
|
+
}.to raise_error("Invalid attribute name(s) found (invalid, notgood) when defining a requirement of type all for HashWithStrings .The only existing attributes are [:name, :something]")
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
416
437
|
context '.dump' do
|
417
438
|
|
418
439
|
let(:value) { {one: 1, two: 2} }
|
@@ -505,6 +526,137 @@ context 'attributes' do
|
|
505
526
|
|
506
527
|
end
|
507
528
|
|
529
|
+
|
530
|
+
context 'with requirements defined' do
|
531
|
+
let(:type) { Attributor::Hash.construct(block) }
|
532
|
+
|
533
|
+
context 'using requires' do
|
534
|
+
let(:block) do
|
535
|
+
proc do
|
536
|
+
key 'name', String
|
537
|
+
key 'consistency', Attributor::Boolean
|
538
|
+
key 'availability', Attributor::Boolean
|
539
|
+
key 'partitioning', Attributor::Boolean
|
540
|
+
requires 'consistency', 'availability'
|
541
|
+
requires.all 'name' # Just to show that it is equivalent to 'requires'
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
it 'complains not all the listed elements are set (false or true)' do
|
546
|
+
errors = type.new('name' => 'CAP').validate
|
547
|
+
errors.should have(2).items
|
548
|
+
['consistency','availability'].each do |name|
|
549
|
+
errors.should include("Key #{name} is required for $.")
|
550
|
+
end
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
context 'using at_least(n)' do
|
555
|
+
let(:block) do
|
556
|
+
proc do
|
557
|
+
key 'name', String
|
558
|
+
key 'consistency', Attributor::Boolean
|
559
|
+
key 'availability', Attributor::Boolean
|
560
|
+
key 'partitioning', Attributor::Boolean
|
561
|
+
requires.at_least(2).of 'consistency', 'availability', 'partitioning'
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
it 'complains if less than 2 in the group are set (false or true)' do
|
566
|
+
errors = type.new('name' => 'CAP', 'consistency' => false).validate
|
567
|
+
errors.should have(1).items
|
568
|
+
errors.should include(
|
569
|
+
"At least 2 keys out of [\"consistency\", \"availability\", \"partitioning\"] are required to be passed in for $. Found [\"consistency\"]"
|
570
|
+
)
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
context 'using at_most(n)' do
|
575
|
+
let(:block) do
|
576
|
+
proc do
|
577
|
+
key 'name', String
|
578
|
+
key 'consistency', Attributor::Boolean
|
579
|
+
key 'availability', Attributor::Boolean
|
580
|
+
key 'partitioning', Attributor::Boolean
|
581
|
+
requires.at_most(2).of 'consistency', 'availability', 'partitioning'
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
it 'complains if more than 2 in the group are set (false or true)' do
|
586
|
+
errors = type.new('name' => 'CAP', 'consistency' => false, 'availability' => true, 'partitioning' => false).validate
|
587
|
+
errors.should have(1).items
|
588
|
+
errors.should include("At most 2 keys out of [\"consistency\", \"availability\", \"partitioning\"] can be passed in for $. Found [\"consistency\", \"availability\", \"partitioning\"]")
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
context 'using exactly(n)' do
|
593
|
+
let(:block) do
|
594
|
+
proc do
|
595
|
+
key 'name', String
|
596
|
+
key 'consistency', Attributor::Boolean
|
597
|
+
key 'availability', Attributor::Boolean
|
598
|
+
key 'partitioning', Attributor::Boolean
|
599
|
+
requires.exactly(1).of 'consistency', 'availability', 'partitioning'
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
it 'complains if less than 1 in the group are set (false or true)' do
|
604
|
+
errors = type.new('name' => 'CAP').validate
|
605
|
+
errors.should have(1).items
|
606
|
+
errors.should include("Exactly 1 of the following keys [\"consistency\", \"availability\", \"partitioning\"] are required for $. Found 0 instead: []")
|
607
|
+
end
|
608
|
+
it 'complains if more than 1 in the group are set (false or true)' do
|
609
|
+
errors = type.new('name' => 'CAP', 'consistency' => false, 'availability' => true).validate
|
610
|
+
errors.should have(1).items
|
611
|
+
errors.should include("Exactly 1 of the following keys [\"consistency\", \"availability\", \"partitioning\"] are required for $. Found 2 instead: [\"consistency\", \"availability\"]")
|
612
|
+
end
|
613
|
+
end
|
614
|
+
|
615
|
+
context 'using exclusive' do
|
616
|
+
let(:block) do
|
617
|
+
proc do
|
618
|
+
key 'name', String
|
619
|
+
key 'consistency', Attributor::Boolean
|
620
|
+
key 'availability', Attributor::Boolean
|
621
|
+
key 'partitioning', Attributor::Boolean
|
622
|
+
requires.exclusive 'consistency', 'availability', 'partitioning'
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
it 'complains if two or more in the group are set (false or true)' do
|
627
|
+
errors = type.new('name' => 'CAP', 'consistency' => false, 'availability' => true).validate
|
628
|
+
errors.should have(1).items
|
629
|
+
errors.should include("keys [\"consistency\", \"availability\"] are mutually exclusive for $.")
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
context 'through a block' do
|
634
|
+
let(:block) do
|
635
|
+
proc do
|
636
|
+
key 'name', String
|
637
|
+
key 'consistency', Attributor::Boolean
|
638
|
+
key 'availability', Attributor::Boolean
|
639
|
+
key 'partitioning', Attributor::Boolean
|
640
|
+
requires do
|
641
|
+
all 'name'
|
642
|
+
all.of 'name' # Equivalent to .all
|
643
|
+
at_least(1).of 'consistency', 'availability', 'partitioning'
|
644
|
+
end
|
645
|
+
# Silly example, just to show that block and inline requires can be combined
|
646
|
+
requires.at_most(3).of 'consistency', 'availability', 'partitioning'
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
it 'complains not all the listed elements are set (false or true)' do
|
651
|
+
errors = type.new('name' => 'CAP').validate
|
652
|
+
errors.should have(1).items
|
653
|
+
errors.should include(
|
654
|
+
"At least 1 keys out of [\"consistency\", \"availability\", \"partitioning\"] are required to be passed in for $. Found none"
|
655
|
+
)
|
656
|
+
end
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
508
660
|
end
|
509
661
|
|
510
662
|
context 'in an Attribute' do
|
@@ -539,6 +691,13 @@ context 'attributes' do
|
|
539
691
|
key '1', Integer, min: 1, max: 20
|
540
692
|
key 'some_date', DateTime
|
541
693
|
key 'defaulted', String, default: 'default value'
|
694
|
+
requires do
|
695
|
+
all.of '1','some_date'
|
696
|
+
exclusive 'some_date', 'defaulted'
|
697
|
+
at_least(1).of 'a string', 'some_date'
|
698
|
+
at_most(2).of 'a string', 'some_date'
|
699
|
+
exactly(1).of 'a string', 'some_date'
|
700
|
+
end
|
542
701
|
end
|
543
702
|
end
|
544
703
|
|
@@ -548,7 +707,9 @@ context 'attributes' do
|
|
548
707
|
description[:name].should eq('Hash')
|
549
708
|
description[:key].should eq(type:{name: 'String', id: 'Attributor-String', family: 'string'})
|
550
709
|
description.should_not have_key(:value)
|
710
|
+
end
|
551
711
|
|
712
|
+
it 'describes the type attributes correctly' do
|
552
713
|
attrs = description[:attributes]
|
553
714
|
|
554
715
|
attrs['a string'].should eq(type: {name: 'String', id: 'Attributor-String', family: 'string'} )
|
@@ -557,6 +718,49 @@ context 'attributes' do
|
|
557
718
|
attrs['defaulted'].should eq(type: {name: 'String', id: 'Attributor-String', family: 'string'}, default: 'default value')
|
558
719
|
end
|
559
720
|
|
721
|
+
it 'describes the type requirements correctly' do
|
722
|
+
|
723
|
+
reqs = description[:requirements]
|
724
|
+
reqs.should be_kind_of(Array)
|
725
|
+
reqs.size.should be(5)
|
726
|
+
reqs.should include( type: :all, attributes: ['1','some_date'] )
|
727
|
+
reqs.should include( type: :exclusive, attributes: ['some_date','defaulted'] )
|
728
|
+
reqs.should include( type: :at_least, attributes: ['a string','some_date'], count: 1 )
|
729
|
+
reqs.should include( type: :at_most, attributes: ['a string','some_date'], count: 2 )
|
730
|
+
reqs.should include( type: :exactly, attributes: ['a string','some_date'], count: 1 )
|
731
|
+
end
|
732
|
+
|
733
|
+
context 'merging requires.all with attribute required: true' do
|
734
|
+
let(:block) do
|
735
|
+
proc do
|
736
|
+
key 'required string', String, required: true
|
737
|
+
key '1', Integer
|
738
|
+
key 'some_date', DateTime
|
739
|
+
requires do
|
740
|
+
all.of 'some_date'
|
741
|
+
end
|
742
|
+
end
|
743
|
+
end
|
744
|
+
it 'includes attributes with required: true into the :all requirements' do
|
745
|
+
req_all = description[:requirements].select{|r| r[:type] == :all}.first
|
746
|
+
req_all[:attributes].should include( 'required string','some_date' )
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
750
|
+
context 'creates the :all requirement when any attribute has required: true' do
|
751
|
+
let(:block) do
|
752
|
+
proc do
|
753
|
+
key 'required string', String, required: true
|
754
|
+
key 'required integer', Integer, required: true
|
755
|
+
end
|
756
|
+
end
|
757
|
+
it 'includes attributes with required: true into the :all requirements' do
|
758
|
+
req_all = description[:requirements].select{|r| r[:type] == :all}.first
|
759
|
+
req_all.should_not be(nil)
|
760
|
+
req_all[:attributes].should include( 'required string','required integer' )
|
761
|
+
end
|
762
|
+
end
|
763
|
+
|
560
764
|
context 'with an example' do
|
561
765
|
let(:example){ type.example }
|
562
766
|
|
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.
|
4
|
+
version: 4.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josep M. Blanquer
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-10-
|
12
|
+
date: 2015-10-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: hashie
|
@@ -293,6 +293,7 @@ files:
|
|
293
293
|
- lib/attributor/extras/field_selector/transformer.rb
|
294
294
|
- lib/attributor/families/numeric.rb
|
295
295
|
- lib/attributor/families/temporal.rb
|
296
|
+
- lib/attributor/hash_dsl_compiler.rb
|
296
297
|
- lib/attributor/type.rb
|
297
298
|
- lib/attributor/types/bigdecimal.rb
|
298
299
|
- lib/attributor/types/boolean.rb
|
@@ -323,6 +324,7 @@ files:
|
|
323
324
|
- spec/dsl_compiler_spec.rb
|
324
325
|
- spec/extras/field_selector/field_selector_spec.rb
|
325
326
|
- spec/families_spec.rb
|
327
|
+
- spec/hash_dsl_compiler_spec.rb
|
326
328
|
- spec/spec_helper.rb
|
327
329
|
- spec/support/hashes.rb
|
328
330
|
- spec/support/models.rb
|
@@ -378,6 +380,7 @@ test_files:
|
|
378
380
|
- spec/dsl_compiler_spec.rb
|
379
381
|
- spec/extras/field_selector/field_selector_spec.rb
|
380
382
|
- spec/families_spec.rb
|
383
|
+
- spec/hash_dsl_compiler_spec.rb
|
381
384
|
- spec/spec_helper.rb
|
382
385
|
- spec/support/hashes.rb
|
383
386
|
- spec/support/models.rb
|