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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 28036c8d33153157e7118cae29dd5d0736248cb2
4
- data.tar.gz: ea81f5e962a1f8f63f34dad0e41e4bd1899aebb2
3
+ metadata.gz: 91fa7d928579dfcb275a39160a5a97b3e2ed3714
4
+ data.tar.gz: f2e689081dfe7e08ff69dd6d5d497fbf9fbadf64
5
5
  SHA512:
6
- metadata.gz: dacc030e86045803473c59ad89fce1d3ae1e7dd12fab67d42dbcde02e23477861e4cd45b0fdd3fd0d9628daec92b57d812d55f3ed2b60d10f86a2372446ff8c3
7
- data.tar.gz: 27c876734a439ff0df35a2970dbd57b90821b16ab9710d99391b2948be0052ffb91d18b2730d99fbbb827739ad742aaedffa2c66b5a388de12b90e9e84ec58b5
6
+ metadata.gz: cb003537a99b92977798f35b5b8655e6baaec686e9beb03bb7093413c6fcc99b2bfe00587532ebc8957c30ff9a8de04311bcb45b9be7fdfaa038b9206f6e2701
7
+ data.tar.gz: 6ae7ade75b1cf99da3a1c317e26e758f92657617f158c47d4778471bee2b5d2e14e623dd111539994404263e4dc2c9bc19b7490af21ad84adba926d114157391
@@ -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...)
@@ -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
@@ -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] || DSLCompiler
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
- # FIXME: change to :keys when the praxis doc browser supports displaying those. or josep's demo is over.
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
 
@@ -43,6 +43,7 @@ module Attributor
43
43
  @key_attribute = ka
44
44
  @value_attribute = va
45
45
 
46
+ @requirements = []
46
47
  @error = false
47
48
  end
48
49
  end
@@ -1,3 +1,3 @@
1
1
  module Attributor
2
- VERSION = "4.1.0"
2
+ VERSION = "4.2.0"
3
3
  end
@@ -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
@@ -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
@@ -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.1.0
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-19 00:00:00.000000000 Z
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