attributor 5.1.0 → 5.5

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -3
  3. data/CHANGELOG.md +145 -135
  4. data/attributor.gemspec +5 -6
  5. data/lib/attributor.rb +17 -2
  6. data/lib/attributor/attribute.rb +39 -9
  7. data/lib/attributor/dsl_compiler.rb +17 -9
  8. data/lib/attributor/exceptions.rb +5 -0
  9. data/lib/attributor/extras/field_selector.rb +4 -0
  10. data/lib/attributor/families/numeric.rb +19 -6
  11. data/lib/attributor/families/temporal.rb +16 -9
  12. data/lib/attributor/hash_dsl_compiler.rb +6 -6
  13. data/lib/attributor/smart_attribute_selector.rb +149 -0
  14. data/lib/attributor/type.rb +27 -4
  15. data/lib/attributor/types/bigdecimal.rb +7 -2
  16. data/lib/attributor/types/boolean.rb +7 -2
  17. data/lib/attributor/types/class.rb +2 -2
  18. data/lib/attributor/types/collection.rb +22 -5
  19. data/lib/attributor/types/container.rb +3 -3
  20. data/lib/attributor/types/csv.rb +5 -1
  21. data/lib/attributor/types/date.rb +9 -3
  22. data/lib/attributor/types/date_time.rb +8 -2
  23. data/lib/attributor/types/float.rb +4 -3
  24. data/lib/attributor/types/hash.rb +105 -21
  25. data/lib/attributor/types/integer.rb +7 -1
  26. data/lib/attributor/types/model.rb +2 -2
  27. data/lib/attributor/types/object.rb +5 -0
  28. data/lib/attributor/types/polymorphic.rb +3 -2
  29. data/lib/attributor/types/string.rb +20 -1
  30. data/lib/attributor/types/struct.rb +1 -1
  31. data/lib/attributor/types/symbol.rb +5 -0
  32. data/lib/attributor/types/tempfile.rb +4 -0
  33. data/lib/attributor/types/time.rb +7 -3
  34. data/lib/attributor/types/uri.rb +9 -1
  35. data/lib/attributor/version.rb +1 -1
  36. data/spec/attribute_spec.rb +42 -7
  37. data/spec/dsl_compiler_spec.rb +16 -6
  38. data/spec/extras/field_selector/field_selector_spec.rb +9 -0
  39. data/spec/hash_dsl_compiler_spec.rb +2 -2
  40. data/spec/smart_attribute_selector_spec.rb +272 -0
  41. data/spec/support/integers.rb +7 -0
  42. data/spec/type_spec.rb +1 -1
  43. data/spec/types/bigdecimal_spec.rb +8 -0
  44. data/spec/types/boolean_spec.rb +10 -0
  45. data/spec/types/class_spec.rb +0 -1
  46. data/spec/types/collection_spec.rb +16 -0
  47. data/spec/types/date_spec.rb +9 -0
  48. data/spec/types/date_time_spec.rb +9 -0
  49. data/spec/types/float_spec.rb +8 -0
  50. data/spec/types/hash_spec.rb +181 -9
  51. data/spec/types/integer_spec.rb +10 -1
  52. data/spec/types/model_spec.rb +14 -3
  53. data/spec/types/string_spec.rb +10 -0
  54. data/spec/types/temporal_spec.rb +5 -1
  55. data/spec/types/time_spec.rb +9 -0
  56. data/spec/types/uri_spec.rb +9 -0
  57. metadata +24 -34
@@ -1,7 +1,9 @@
1
1
 
2
2
 
3
3
  module Attributor
4
- class Integer < Numeric
4
+ class Integer
5
+ include Attributor::Numeric
6
+
5
7
  EXAMPLE_RANGE = 1000
6
8
 
7
9
  def self.native_type
@@ -53,5 +55,9 @@ module Attributor
53
55
  end
54
56
  true
55
57
  end
58
+
59
+ def self.json_schema_type
60
+ :integer
61
+ end
56
62
  end
57
63
  end
@@ -105,7 +105,7 @@ module Attributor
105
105
  result = new
106
106
  result.extend(ExampleMixin)
107
107
 
108
- result.lazy_attributes = example_contents(context, result, values)
108
+ result.lazy_attributes = example_contents(context, result, **values)
109
109
  else
110
110
  result = new
111
111
  end
@@ -192,7 +192,7 @@ module Attributor
192
192
  next
193
193
  end
194
194
 
195
- hash[name.to_sym] = attribute.dump(value, context: context + [name])
195
+ hash[name.to_sym] = attribute.dump(value, context: context + [name], **_opts)
196
196
  end
197
197
  ensure
198
198
  @dumping = false
@@ -13,5 +13,10 @@ module Attributor
13
13
  def self.example(_context = nil, options: {})
14
14
  'An Object'
15
15
  end
16
+
17
+ def self.json_schema_type
18
+ :object #FIXME: not sure this is the most appropriate, since an Attributor::Object can be anything
19
+ end
20
+
16
21
  end
17
22
  end
@@ -1,4 +1,3 @@
1
- require 'active_support'
2
1
 
3
2
  require_relative '../exceptions'
4
3
 
@@ -95,7 +94,9 @@ module Attributor
95
94
  value
96
95
  elsif value.is_a?(::String)
97
96
  decode_json(value, context)
98
- elsif value.respond_to?(:to_hash)
97
+ elsif value.respond_to?(:to_h)
98
+ value.to_h
99
+ elsif value.respond_to?(:to_hash) # Deprecate this in lieu of to_h only?
99
100
  value.to_hash
100
101
  else
101
102
  raise Attributor::IncompatibleTypeError, context: context, value_type: value.class, type: self
@@ -8,7 +8,7 @@ module Attributor
8
8
 
9
9
  def self.load(value, context = Attributor::DEFAULT_ROOT_CONTEXT, **options)
10
10
  if value.is_a?(Enumerable)
11
- raise IncompatibleTypeError, context: context, value_type: value.class, type: self
11
+ raise IncompatibleTypeError.new(context: context, value_type: value.class, type: self)
12
12
  end
13
13
 
14
14
  value && String(value)
@@ -32,5 +32,24 @@ module Attributor
32
32
  def self.family
33
33
  'string'
34
34
  end
35
+
36
+ def self.json_schema_type
37
+ :string
38
+ end
39
+
40
+ # TODO: we're passing the attribute options for now...might need to rethink ...although these are type-specific...
41
+ # TODO: multipleOf, minimum, maximum, exclusiveMinimum and exclusiveMaximum
42
+ def self.as_json_schema( shallow: false, example: nil, attribute_options: {} )
43
+ h = super
44
+ opts = ( self.respond_to?(:options) ) ? self.options.merge( attribute_options ) : attribute_options
45
+ h[:pattern] = self.human_readable_regexp(opts[:regexp]) if opts[:regexp]
46
+ # TODO: minLength, maxLength
47
+ h
48
+ end
49
+
50
+ def self.human_readable_regexp( reg )
51
+ return $1 if reg.to_s =~ /\(\?[^:]+:(.+)\)/
52
+ reg
53
+ end
35
54
  end
36
55
  end
@@ -26,7 +26,7 @@ module Attributor
26
26
  end
27
27
 
28
28
  ::Class.new(self) do
29
- attributes options, &attribute_definition
29
+ attributes **options, &attribute_definition
30
30
  end
31
31
  end
32
32
 
@@ -19,5 +19,10 @@ module Attributor
19
19
  def self.family
20
20
  String.family
21
21
  end
22
+
23
+ def self.json_schema_type
24
+ :string
25
+ end
26
+
22
27
  end
23
28
  end
@@ -38,5 +38,9 @@ module Attributor
38
38
  def self.family
39
39
  String.family
40
40
  end
41
+
42
+ def self.json_schema_type
43
+ :string
44
+ end
41
45
  end
42
46
  end
@@ -1,8 +1,8 @@
1
1
  require 'date'
2
2
 
3
3
  module Attributor
4
- class Time < Temporal
5
- include Type
4
+ class Time
5
+ include Temporal
6
6
 
7
7
  def self.native_type
8
8
  ::Time
@@ -29,11 +29,15 @@ module Attributor
29
29
  begin
30
30
  return ::Time.parse(value)
31
31
  rescue ArgumentError
32
- raise Attributor::DeserializationError, context: context, from: value.class, encoding: 'Time', value: value
32
+ raise Attributor::DeserializationError.new(context: context, from: value.class, encoding: 'Time', value: value)
33
33
  end
34
34
  else
35
35
  raise CoercionError, context: context, from: value.class, to: self, value: value
36
36
  end
37
37
  end
38
+
39
+ def self.json_schema_string_format
40
+ :time
41
+ end
38
42
  end
39
43
  end
@@ -22,6 +22,14 @@ module Attributor
22
22
  ::URI::Generic
23
23
  end
24
24
 
25
+ def self.json_schema_type
26
+ :string
27
+ end
28
+
29
+ def self.json_schema_string_format
30
+ :uri
31
+ end
32
+
25
33
  def self.example(_context = nil, options: {})
26
34
  URI(Randgen.uri)
27
35
  end
@@ -34,7 +42,7 @@ module Attributor
34
42
  when ::String
35
43
  URI(value)
36
44
  else
37
- raise CoercionError, context: context, from: value.class, to: self, value: value
45
+ raise CoercionError.new(context: context, from: value.class, to: self, value: value)
38
46
  end
39
47
  end
40
48
 
@@ -1,3 +1,3 @@
1
1
  module Attributor
2
- VERSION = '5.1.0'.freeze
2
+ VERSION = '5.5'.freeze
3
3
  end
@@ -36,17 +36,48 @@ describe Attributor::Attribute do
36
36
  it { should eq other_attribute }
37
37
  end
38
38
 
39
+ context 'describe_json_schema' do
40
+ let(:type) { PositiveIntegerType }
41
+
42
+ let(:attribute_options) do
43
+ {
44
+ values: [1,20],
45
+ description: "something",
46
+ example: 20,
47
+ max: 1000,
48
+ default: 1
49
+ }
50
+ end
51
+
52
+ context 'reports all of the possible attributes' do
53
+ let(:js){ subject.as_json_schema(example: 20) }
54
+
55
+ it 'including the attribute-specific ones' do
56
+ expect(js[:enum]).to eq( [1,20])
57
+ expect(js[:description]).to eq( "something")
58
+ expect(js[:default]).to eq(1)
59
+ expect(js[:example]).to eq(20)
60
+ end
61
+
62
+ it 'as well as the type-specific ones' do
63
+ expect(js[:type]).to eq(:integer)
64
+ end
65
+ end
66
+
67
+ end
68
+
39
69
  context 'describe' do
40
- let(:attribute_options) { { required: true, values: ['one'], description: 'something', min: 0 } }
70
+ let(:attribute_options) { {required: true, values: ['one'], description: "something", min: 0} }
41
71
  let(:expected) do
42
- h = { type: { name: 'String', id: type.id, family: type.family } }
43
- common = attribute_options.select { |k, _v| Attributor::Attribute::TOP_LEVEL_OPTIONS.include? k }
44
- h.merge!(common)
45
- h[:options] = { min: 0 }
72
+ h = {type: {name: 'String', id: type.id, family: type.family}}
73
+ common = attribute_options.select{|k,v| Attributor::Attribute::TOP_LEVEL_OPTIONS.include? k }
74
+ h.merge!( common )
75
+ h[:options] = {:min => 0 }
46
76
  h
47
77
  end
48
78
 
49
- its(:describe) { should eq expected }
79
+ # It has both the type-included options (min) as well as the attribute options (max)
80
+ its(:describe) { should == expected }
50
81
 
51
82
  context 'with example options' do
52
83
  let(:attribute_options) { { description: 'something', example: 'ex_def' } }
@@ -313,7 +344,11 @@ describe Attributor::Attribute do
313
344
  let(:value) { '1' }
314
345
 
315
346
  it 'delegates to type.load' do
316
- expect(type).to receive(:load).with(value, context, {})
347
+ # Need to add the "anything" of the 3rd element, as in ruby < 2.7 it comes as an empty hash
348
+ expect(type).to receive(:load) do |v, c, _other|
349
+ expect(v).to eq(value)
350
+ expect(c).to eq(context)
351
+ end
317
352
  attribute.load(value, context)
318
353
  end
319
354
 
@@ -4,7 +4,7 @@ describe Attributor::DSLCompiler do
4
4
  let(:target) { double('model', attributes: {}) }
5
5
 
6
6
  let(:dsl_compiler_options) { {} }
7
- subject(:dsl_compiler) { Attributor::DSLCompiler.new(target, dsl_compiler_options) }
7
+ subject(:dsl_compiler) { Attributor::DSLCompiler.new(target, **dsl_compiler_options) }
8
8
 
9
9
  let(:attribute_name) { :name }
10
10
  let(:type) { Attributor::String }
@@ -35,7 +35,7 @@ describe Attributor::DSLCompiler do
35
35
 
36
36
  it 'creates an attribute given a name, type, and options' do
37
37
  expect(Attributor::Attribute).to receive(:new).with(expected_type, expected_options)
38
- dsl_compiler.attribute(attribute_name, type, attribute_options)
38
+ dsl_compiler.attribute(attribute_name, type, **attribute_options)
39
39
  end
40
40
  end
41
41
 
@@ -60,11 +60,21 @@ describe Attributor::DSLCompiler do
60
60
  end
61
61
 
62
62
  it 'creates an attribute with the inherited type and merged options' do
63
- dsl_compiler.attribute(attribute_name, attribute_options)
63
+ dsl_compiler.attribute(attribute_name, **attribute_options)
64
64
  end
65
65
 
66
66
  it 'accepts explicit nil type' do
67
- dsl_compiler.attribute(attribute_name, nil, attribute_options)
67
+ dsl_compiler.attribute(attribute_name, nil, **attribute_options)
68
+ end
69
+
70
+ context 'but with the attribute also specifying a reference' do
71
+ let(:attribute_options) { { reference: Attributor::CSV } }
72
+ let(:expected_type) { Attributor::CSV }
73
+ let(:expected_options) { attribute_options }
74
+ it 'attribute reference takes precedence over the compiler one (and merges no options)' do
75
+ expect(attribute_options[:reference]).to_not eq(dsl_compiler_options[:reference])
76
+ dsl_compiler.attribute(attribute_name, **attribute_options)
77
+ end
68
78
  end
69
79
  end
70
80
 
@@ -103,12 +113,12 @@ describe Attributor::DSLCompiler do
103
113
  it 'sets the type of the attribute to Struct' do
104
114
  expect(Attributor::Attribute).to receive(:new)
105
115
  .with(expected_type, description: 'The turkey', reference: Turkey)
106
- dsl_compiler.attribute(attribute_name, attribute_options, &attribute_block)
116
+ dsl_compiler.attribute(attribute_name, **attribute_options, &attribute_block)
107
117
  end
108
118
 
109
119
  it 'passes the correct reference to the created attribute' do
110
120
  expect(Attributor::Attribute).to receive(:new).with(expected_type, expected_options)
111
- dsl_compiler.attribute(attribute_name, type, attribute_options, &attribute_block)
121
+ dsl_compiler.attribute(attribute_name, type, **attribute_options, &attribute_block)
112
122
  end
113
123
  end
114
124
  end
@@ -33,4 +33,13 @@ describe Attributor::FieldSelector do
33
33
  end
34
34
  end
35
35
  end
36
+
37
+ context '.as_json_schema' do
38
+ subject(:js){ type.as_json_schema }
39
+ it 'adds the right attributes' do
40
+ expect(js.keys).to include(:type, :'x-type_name')
41
+ expect(js[:type]).to eq(:string)
42
+ expect(js[:'x-type_name']).to eq('FieldSelector')
43
+ end
44
+ end
36
45
  end
@@ -4,7 +4,7 @@ describe Attributor::HashDSLCompiler do
4
4
  let(:target) { double('model', attributes: {}) }
5
5
 
6
6
  let(:dsl_compiler_options) { {} }
7
- subject(:dsl_compiler) { Attributor::HashDSLCompiler.new(target, dsl_compiler_options) }
7
+ subject(:dsl_compiler) { Attributor::HashDSLCompiler.new(target, **dsl_compiler_options) }
8
8
 
9
9
  it 'returns the requirements DSL attached to the right target' do
10
10
  req_dsl = dsl_compiler._requirements_dsl
@@ -103,7 +103,7 @@ describe Attributor::HashDSLCompiler do
103
103
  end
104
104
 
105
105
  context 'Requirement#validate' do
106
- let(:requirement) { req_class.new(arguments) }
106
+ let(:requirement) { req_class.new(**arguments) }
107
107
  let(:subject) { requirement.validate(value, ['$'], nil) }
108
108
 
109
109
  context 'for :all' do
@@ -0,0 +1,272 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ describe Attributor::SmartAttributeSelector do
4
+
5
+ let(:type) { Attributor::Hash.construct(block) }
6
+ let(:block) do
7
+ proc do
8
+ key :req1, String
9
+ key :req2, String
10
+ key :exc3, String
11
+ key :exc4, String
12
+ key :least1, String
13
+ key :least2, String
14
+ key :exact1, String
15
+ key :exact2, String
16
+ key :most1, String
17
+ key :most2, String
18
+
19
+ requires.all :req1, :req2
20
+ requires.exclusive :exc3, :exc4
21
+ requires.at_least(2).of :least1, :least2
22
+ requires.exactly(1).of :exc3, :exact1, :exact2
23
+ requires.at_most(1).of :most1, :most2
24
+ requires.at_least(1).of :exc4, :exc3
25
+ end
26
+ end
27
+
28
+ let(:reqs){ type.requirements.map(&:describe) }
29
+ let(:attributes){ [] }
30
+ let(:values){ {} }
31
+ let(:remaining_attrs){ [] }
32
+
33
+ subject(:selector){ Attributor::SmartAttributeSelector.new( reqs, attributes , values) }
34
+ let(:accepted_attrs){ [] }
35
+
36
+ after do
37
+ expect(subject.accepted).to contain_exactly(*accepted_attrs)
38
+ expect(subject.remaining).to contain_exactly(*remaining_attrs)
39
+ end
40
+
41
+ context 'process' do
42
+ let(:accepted_attrs){ [:req1, :req2, :exc3, :least1, :least2, :most1] }
43
+ it 'aggregates results from the different requirements' do
44
+ expect(subject).to receive(:process_required).once.and_call_original
45
+ expect(subject).to receive(:process_exclusive).once.and_call_original
46
+ expect(subject).to receive(:process_at_least).once.and_call_original
47
+ expect(subject).to receive(:process_exactly).once.and_call_original
48
+ expect(subject).to receive(:process_at_most).once.and_call_original
49
+ subject.process
50
+ end
51
+ end
52
+
53
+ context 'process_required' do
54
+ context 'processing only required attrs' do
55
+ let(:block) do
56
+ proc do
57
+ key :req1, String
58
+ key :req2, String
59
+ requires.all :req1, :req2
60
+ end
61
+ end
62
+ let(:accepted_attrs){ [:req1,:req2] }
63
+ it 'processes required' do
64
+ subject.process_required
65
+ end
66
+ end
67
+
68
+ context 'processing some required attrs, others attrs without reqs' do
69
+ let(:block) do
70
+ proc do
71
+ key :req1, String
72
+ key :req2, String
73
+ requires.all :req1
74
+ end
75
+ end
76
+ let(:accepted_attrs){ [:req1] }
77
+
78
+ it 'processes required' do
79
+ subject.process_required
80
+ end
81
+ end
82
+ end
83
+
84
+
85
+ context 'process_exclusive' do
86
+ context 'uses exclusive,at_most(1) and exactly(1)' do
87
+ let(:block) do
88
+ proc do
89
+ key :req1, String
90
+ key :req2, String
91
+ key :req3, String
92
+ key :req4, String
93
+ key :req5, String
94
+ key :req6, String
95
+
96
+ requires.exclusive :req1, :req2
97
+ requires.at_most(1).of :req3, :req4
98
+ requires.exactly(1).of :req5, :req6
99
+ end
100
+ end
101
+ let(:accepted_attrs){ [:req1,:req3,:req5] }
102
+ it 'processes required' do
103
+ expect(subject).to receive(:process_exclusive_set).with([:req1, :req2]).and_call_original
104
+ expect(subject).to receive(:process_exclusive_set).with([:req3, :req4]).and_call_original
105
+ expect(subject).to receive(:process_exclusive_set).with([:req5, :req6]).and_call_original
106
+
107
+ subject.process_exclusive
108
+ end
109
+ end
110
+ end
111
+
112
+
113
+
114
+ context 'internal functions' do
115
+
116
+ context 'process_exclusive_set' do
117
+ context 'picks the first of the set bans the rest' do
118
+ let(:set){ [:req1, :req2] }
119
+ let(:accepted_attrs){ [:req1] }
120
+
121
+ it 'processes required' do
122
+ subject.process_exclusive_set( set )
123
+ expect(subject.banned).to eq([:req2])
124
+ end
125
+ end
126
+
127
+ context 'picks the first not banned, and bans the rest' do
128
+ let(:set){ [:req2, :req3] }
129
+ let(:accepted_attrs){ [:req3] }
130
+
131
+ it 'processes required' do
132
+ subject.banned = [:req2] # Explicitly ban one
133
+ subject.process_exclusive_set( set )
134
+ expect(subject.banned).to eq([:req2])
135
+ end
136
+ end
137
+
138
+ context 'finds it unfeasible' do
139
+ let(:set){ [:req2, :req3] }
140
+
141
+ it 'processes required' do
142
+ subject.banned = [:req2, :req3] # Ban them all
143
+ expect{
144
+ subject.process_exclusive_set( set )
145
+ }.to raise_error(Attributor::UnfeasibleRequirementsError)
146
+ end
147
+ it 'unless the set was empty to begin with' do
148
+ expect{
149
+ subject.process_exclusive_set( [] )
150
+ }.to_not raise_error
151
+ end
152
+ end
153
+
154
+ context 'favors attributes with values' do
155
+ let(:values){ {req2: 'foo'} }
156
+ let(:set){ [:req1, :req2] }
157
+ let(:accepted_attrs){ [:req2] }
158
+
159
+ it 'processes required' do
160
+ subject.process_exclusive_set( set )
161
+ expect(subject.banned).to eq([:req1])
162
+ end
163
+ end
164
+
165
+ context 'manages the remaining set' do
166
+ let(:attributes){ [:req1, :req2, :req3] }
167
+ let(:set){ [:req1, :req2] }
168
+ let(:accepted_attrs){ [:req1] }
169
+ let(:remaining_attrs){ [:req3] }
170
+
171
+ it 'processes required' do
172
+ subject.process_exclusive_set( set )
173
+ expect(subject.banned).to eq([:req2])
174
+ end
175
+ end
176
+ end
177
+
178
+ context 'process_at_least_set' do
179
+ context 'picks the count in order' do
180
+ let(:set){ [:req1, :req2, :req3] }
181
+ let(:accepted_attrs){ [:req1,:req2] }
182
+
183
+ it 'processes required' do
184
+ subject.process_at_least_set( set,2 )
185
+ end
186
+ end
187
+
188
+ context 'picks the count in order, skipping banned' do
189
+ let(:set){ [:req1, :req2, :req3] }
190
+ let(:accepted_attrs){ [:req1,:req3] }
191
+
192
+ it 'processes required' do
193
+ subject.banned = [:req2] # Explicitly ban one
194
+ subject.process_at_least_set( set,2 )
195
+ end
196
+ end
197
+
198
+ context 'finds it unfeasible' do
199
+ let(:set){ [:req1, :req2, :req3] }
200
+
201
+ it 'processes required' do
202
+ expect{
203
+ subject.process_at_least_set( set,4 )
204
+ }.to raise_error(Attributor::UnfeasibleRequirementsError)
205
+ end
206
+ end
207
+
208
+ context 'favors attributes with values' do
209
+ let(:values){ {req1: 'foo', req3: 'bar'} }
210
+ let(:set){ [:req1, :req2, :req3] }
211
+ let(:accepted_attrs){ [:req1,:req3] }
212
+
213
+ it 'processes required' do
214
+ subject.process_at_least_set( set,2 )
215
+ end
216
+ end
217
+ end
218
+
219
+ context 'process_at_most_set' do
220
+ context 'picks half the max count in attr order' do
221
+ let(:set){ [:req1, :req2, :req3, :req4, :req5] }
222
+ let(:accepted_attrs){ [:req1,:req2] }
223
+
224
+ it 'processes required' do
225
+ subject.process_at_most_set( set,4 )
226
+ end
227
+ end
228
+ context 'favors attributes with values (and refills with others)' do
229
+ let(:values){ {req3: 'foo', req5: 'bar'} }
230
+ let(:set){ [:req1, :req2, :req3, :req4, :req5] }
231
+ let(:accepted_attrs){ [:req3,:req5,:req1] }
232
+
233
+ it 'processes required' do
234
+ subject.process_at_most_set( set,5 )
235
+ end
236
+ end
237
+ end
238
+
239
+ context 'process_exactly_set' do
240
+ context 'picks exact count in attr order' do
241
+ let(:set){ [:req1, :req2, :req3, :req4, :req5] }
242
+ let(:accepted_attrs){ [:req1,:req2] }
243
+
244
+ it 'processes required' do
245
+ subject.process_exactly_set( set, 2 )
246
+ end
247
+ end
248
+ context 'favors attributes with values (and refills with others)' do
249
+ let(:values){ {req3: 'foo'} }
250
+ let(:set){ [:req1, :req2, :req3, :req4, :req5] }
251
+ let(:accepted_attrs){ [:req3,:req1] }
252
+
253
+ it 'processes required' do
254
+ subject.process_exactly_set( set, 2 )
255
+ end
256
+ end
257
+
258
+ context 'finds it unfeasible' do
259
+ let(:set){ [:req1, :req2] }
260
+
261
+ it 'processes required' do
262
+ subject.banned = [:req2] # Explicitly ban one
263
+ expect{
264
+ subject.process_exactly_set( set, 2 )
265
+ }.to raise_error(Attributor::UnfeasibleRequirementsError)
266
+ end
267
+ end
268
+
269
+ end
270
+
271
+ end
272
+ end