attributor 5.1.0 → 5.2.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 +8 -0
- data/attributor.gemspec +1 -1
- data/lib/attributor.rb +1 -0
- data/lib/attributor/dsl_compiler.rb +17 -9
- data/lib/attributor/exceptions.rb +5 -0
- data/lib/attributor/smart_attribute_selector.rb +149 -0
- data/lib/attributor/types/collection.rb +1 -0
- data/lib/attributor/types/hash.rb +23 -3
- data/lib/attributor/version.rb +1 -1
- data/spec/dsl_compiler_spec.rb +10 -0
- data/spec/smart_attribute_selector_spec.rb +272 -0
- data/spec/types/hash_spec.rb +55 -1
- metadata +10 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5472909b262b07126ca30d760aadb3f5bc0d362
|
4
|
+
data.tar.gz: 9e66f91088af8389b60d088dd4d7599acfd74d9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7c963afb8460167b7714ae4dd4ae74f273ef703b11059e354f33f2356adfba3e9433dad3bc4497612beacfcaf67c727249a6e0143b5e45a0219b95e9c61ace0
|
7
|
+
data.tar.gz: ef1e2720295ee7456b08cd2c003e044f686958cb4e6ef3738f617d042c1ed2da0f1180e1cc5e11d0b97e0b74046eeead54f4a5b8f6bd843d4e092ab6662baa2b
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,14 @@
|
|
2
2
|
|
3
3
|
## next
|
4
4
|
|
5
|
+
## 5.2 (12/09/2017)
|
6
|
+
|
7
|
+
* Fixed describing `Hash` with no keys defined, to still use a given example (no example outputted before this)
|
8
|
+
* Fix bug that would occur when defining an attribute carrying a reference object, for which the reference type didn't have `attributes` (for example a Collection).
|
9
|
+
* Allows an attribute to override the reference object through its options of its parent (even when its containing object already has one defined).
|
10
|
+
* Built code to generate examples in a smarter way when complex conditional requirements are specified.
|
11
|
+
|
12
|
+
|
5
13
|
## 5.1
|
6
14
|
|
7
15
|
* Added `Polymorphic` type. See [polymorphics.rb](spec/support/polymorphics.rb) for example usage.
|
data/attributor.gemspec
CHANGED
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_development_dependency 'rspec', '~> 3'
|
27
27
|
spec.add_development_dependency 'rspec-its'
|
28
28
|
spec.add_development_dependency 'rspec-collection_matchers', '~> 1'
|
29
|
-
spec.add_development_dependency('yard'
|
29
|
+
spec.add_development_dependency('yard')
|
30
30
|
spec.add_development_dependency('backports', ['~> 3'])
|
31
31
|
spec.add_development_dependency('yardstick', ['~> 0'])
|
32
32
|
spec.add_development_dependency('bundler', ['>= 0'])
|
data/lib/attributor.rb
CHANGED
@@ -14,6 +14,7 @@ module Attributor
|
|
14
14
|
require_relative 'attributor/dsl_compiler'
|
15
15
|
require_relative 'attributor/hash_dsl_compiler'
|
16
16
|
require_relative 'attributor/attribute_resolver'
|
17
|
+
require_relative 'attributor/smart_attribute_selector'
|
17
18
|
|
18
19
|
require_relative 'attributor/example_mixin'
|
19
20
|
|
@@ -78,6 +78,8 @@ module Attributor
|
|
78
78
|
# end
|
79
79
|
# @api semiprivate
|
80
80
|
def define(name, attr_type = nil, **opts, &block)
|
81
|
+
example_given = opts.key? :example
|
82
|
+
|
81
83
|
# add to existing attribute if present
|
82
84
|
if (existing_attribute = attributes[name])
|
83
85
|
if existing_attribute.attributes
|
@@ -86,27 +88,33 @@ module Attributor
|
|
86
88
|
end
|
87
89
|
end
|
88
90
|
|
89
|
-
# determine inherited attribute
|
90
|
-
|
91
|
-
|
92
|
-
|
91
|
+
# determine inherited type (giving preference to the direct attribute options)
|
92
|
+
inherited_type = opts[:reference]
|
93
|
+
unless inherited_type
|
94
|
+
reference = options[:reference]
|
95
|
+
if reference && reference.respond_to?(:attributes) && reference.attributes.key?(name)
|
96
|
+
inherited_attribute = reference.attributes[name]
|
93
97
|
opts = inherited_attribute.options.merge(opts) unless attr_type
|
94
|
-
|
98
|
+
inherited_type = inherited_attribute.type
|
99
|
+
opts[:reference] = inherited_type if block_given?
|
95
100
|
end
|
96
101
|
end
|
97
102
|
|
98
103
|
# determine attribute type to use
|
99
104
|
if attr_type.nil?
|
100
105
|
if block_given?
|
101
|
-
|
106
|
+
# Don't inherit explicit examples if we've redefined the structure
|
107
|
+
# (but preserve the direct example if given here)
|
108
|
+
opts.delete :example unless example_given
|
109
|
+
attr_type = if inherited_type && inherited_type < Attributor::Collection
|
102
110
|
# override the reference to be the member_attribute's type for collections
|
103
|
-
opts[:reference] =
|
111
|
+
opts[:reference] = inherited_type.member_attribute.type
|
104
112
|
Attributor::Collection.of(Struct)
|
105
113
|
else
|
106
114
|
Attributor::Struct
|
107
115
|
end
|
108
|
-
elsif
|
109
|
-
attr_type =
|
116
|
+
elsif inherited_type
|
117
|
+
attr_type = inherited_type
|
110
118
|
else
|
111
119
|
raise AttributorException, "type for attribute with name: #{name} could not be determined"
|
112
120
|
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module Attributor
|
2
|
+
class SmartAttributeSelector
|
3
|
+
attr_accessor :reqs, :accepted, :banned, :remaining
|
4
|
+
attr_reader :reqs, :accepted, :banned, :remaining, :keys_with_values
|
5
|
+
|
6
|
+
def initialize( reqs , attributes, values)
|
7
|
+
@reqs = reqs.dup
|
8
|
+
@accepted = []
|
9
|
+
@banned = []
|
10
|
+
@remaining = attributes.dup
|
11
|
+
@keys_with_values = values.each_with_object([]){|(k,v),populated| populated.push(k) unless v == nil}
|
12
|
+
end
|
13
|
+
|
14
|
+
def process
|
15
|
+
process_required
|
16
|
+
process_exclusive
|
17
|
+
process_exactly
|
18
|
+
process_at_least
|
19
|
+
process_at_most
|
20
|
+
# Just add the ones that haven't been explicitly rejected
|
21
|
+
self.accepted += self.remaining
|
22
|
+
self.remaining = []
|
23
|
+
self.accepted.uniq!
|
24
|
+
self.accepted
|
25
|
+
end
|
26
|
+
|
27
|
+
def process_required
|
28
|
+
self.reqs = reqs.each_with_object([]) do |req, rest|
|
29
|
+
if req[:type] == :all
|
30
|
+
self.accepted += req[:attributes]
|
31
|
+
self.remaining -= req[:attributes]
|
32
|
+
else
|
33
|
+
rest.push req
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_exclusive
|
39
|
+
self.reqs = reqs.each_with_object([]) do |req, rest|
|
40
|
+
if req[:type] == :exclusive ||
|
41
|
+
(req[:type] == :exactly && req[:count] == 1 ) ||
|
42
|
+
(req[:type] == :at_most && req[:count] == 1 )
|
43
|
+
process_exclusive_set(Array.new(req[:attributes]))
|
44
|
+
else
|
45
|
+
rest.push req
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def process_at_least
|
51
|
+
self.reqs = reqs.each_with_object([]) do |req, rest|
|
52
|
+
if req[:type] == :at_least
|
53
|
+
process_at_least_set(Array.new(req[:attributes]), req[:count])
|
54
|
+
else
|
55
|
+
rest.push req
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def process_at_most
|
61
|
+
self.reqs = reqs.each_with_object([]) do |req, rest|
|
62
|
+
if req[:type] == :at_most && req[:count] > 1 # count=1 is already handled in exclusive
|
63
|
+
process_at_most_set(Array.new(req[:attributes]), req[:count])
|
64
|
+
else
|
65
|
+
rest.push req
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def process_exactly
|
71
|
+
self.reqs = reqs.each_with_object([]) do |req, rest|
|
72
|
+
if req[:type] == :exactly && req[:count] > 1 # count=1 is already handled in exclusive
|
73
|
+
process_exactly_set(Array.new(req[:attributes]), req[:count])
|
74
|
+
else
|
75
|
+
rest.push req
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
#################
|
81
|
+
|
82
|
+
def process_exclusive_set( exclusive_set )
|
83
|
+
feasible = exclusive_set - banned # available ones to pick (that are not banned)
|
84
|
+
# Try to favor attributes that come in with some values, otherwise get the first feasible one
|
85
|
+
preferred = feasible & keys_with_values
|
86
|
+
pick = (preferred.size == 0 ? feasible : preferred).first
|
87
|
+
|
88
|
+
if pick
|
89
|
+
self.accepted.push( pick )
|
90
|
+
else
|
91
|
+
raise UnfeasibleRequirementsError unless exclusive_set.empty?
|
92
|
+
end
|
93
|
+
self.banned += (feasible - [pick])
|
94
|
+
self.remaining -= exclusive_set
|
95
|
+
end
|
96
|
+
|
97
|
+
def process_at_least_set( at_least_set, count)
|
98
|
+
feasible = at_least_set - banned # available ones to pick (that are not banned)
|
99
|
+
preferred = (feasible & keys_with_values)[0,count]
|
100
|
+
# Add more if not enough
|
101
|
+
pick = if preferred.size < count
|
102
|
+
preferred + (feasible - preferred)[0,count-preferred.size]
|
103
|
+
else
|
104
|
+
preferred
|
105
|
+
end
|
106
|
+
|
107
|
+
unless pick.size == count
|
108
|
+
raise UnfeasibleRequirementsError
|
109
|
+
end
|
110
|
+
self.accepted += pick
|
111
|
+
self.remaining -= pick
|
112
|
+
end
|
113
|
+
|
114
|
+
def process_at_most_set( set, count)
|
115
|
+
ceil=(count+1)/2
|
116
|
+
feasible = set - banned # available ones to pick (that are not banned)
|
117
|
+
preferred = (feasible & keys_with_values)[0,ceil]
|
118
|
+
|
119
|
+
pick = if preferred.size < ceil
|
120
|
+
preferred + (feasible - preferred)[0,(ceil)-preferred.size]
|
121
|
+
else
|
122
|
+
preferred
|
123
|
+
end
|
124
|
+
|
125
|
+
self.accepted += pick
|
126
|
+
self.remaining -= pick
|
127
|
+
end
|
128
|
+
|
129
|
+
def process_exactly_set( set, count)
|
130
|
+
feasible = set - banned # available ones to pick (that are not banned)
|
131
|
+
preferred = (feasible & keys_with_values)[0,count]
|
132
|
+
|
133
|
+
pick = if preferred.size < count
|
134
|
+
preferred + (feasible - preferred)[0,count-preferred.size]
|
135
|
+
else
|
136
|
+
preferred
|
137
|
+
end
|
138
|
+
|
139
|
+
unless pick.size == count
|
140
|
+
raise UnfeasibleRequirementsError
|
141
|
+
end
|
142
|
+
|
143
|
+
self.accepted += pick
|
144
|
+
self.remaining -= pick
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
@@ -59,6 +59,7 @@ module Attributor
|
|
59
59
|
|
60
60
|
# generates an example Collection
|
61
61
|
# @return An Array of native type objects conforming to the specified member_type
|
62
|
+
# TODO: ALLOW to pass "values" for the members?...as values: {id: 1, ...}
|
62
63
|
def self.example(context = nil, options: {})
|
63
64
|
result = []
|
64
65
|
size = options[:size] || (rand(3) + 1)
|
@@ -16,7 +16,7 @@ module Attributor
|
|
16
16
|
end
|
17
17
|
|
18
18
|
class Hash
|
19
|
-
MAX_EXAMPLE_DEPTH =
|
19
|
+
MAX_EXAMPLE_DEPTH = 10
|
20
20
|
CIRCULAR_REFERENCE_MARKER = '...'.freeze
|
21
21
|
|
22
22
|
include Container
|
@@ -97,6 +97,13 @@ module Attributor
|
|
97
97
|
@keys
|
98
98
|
end
|
99
99
|
|
100
|
+
def self.requirements
|
101
|
+
if @saved_blocks.any?
|
102
|
+
definition
|
103
|
+
end
|
104
|
+
@requirements
|
105
|
+
end
|
106
|
+
|
100
107
|
def self.definition
|
101
108
|
opts = {
|
102
109
|
key_type: @key_type,
|
@@ -168,11 +175,23 @@ module Attributor
|
|
168
175
|
self
|
169
176
|
end
|
170
177
|
|
178
|
+
|
179
|
+
|
171
180
|
def self.example_contents(context, parent, **values)
|
172
181
|
hash = ::Hash.new
|
173
182
|
example_depth = context.size
|
174
|
-
|
175
|
-
|
183
|
+
# Be smart about what attributes to use for the example: i.e. have into account complex requirements
|
184
|
+
# that might have been defined in the hash like at_most(1).of ..., exactly(2).of ...etc.
|
185
|
+
# But play it safe and default to the previous behavior in case there is any error processing them
|
186
|
+
# ( that is until the SmartAttributeSelector class isn't fully tested and ready for prime time)
|
187
|
+
begin
|
188
|
+
stack = SmartAttributeSelector.new( requirements.map(&:describe), keys.keys , values)
|
189
|
+
selected = stack.process
|
190
|
+
rescue => e
|
191
|
+
selected = keys.keys
|
192
|
+
end
|
193
|
+
|
194
|
+
keys.select{|n,attr| selected.include? n}.each do |sub_attribute_name, sub_attribute|
|
176
195
|
if sub_attribute.attributes
|
177
196
|
# TODO: add option to raise an exception in this case?
|
178
197
|
next if example_depth > MAX_EXAMPLE_DEPTH
|
@@ -436,6 +455,7 @@ module Attributor
|
|
436
455
|
end
|
437
456
|
else
|
438
457
|
hash[:value] = { type: value_type.describe(true) }
|
458
|
+
hash[:example] = example if example
|
439
459
|
end
|
440
460
|
|
441
461
|
hash
|
data/lib/attributor/version.rb
CHANGED
data/spec/dsl_compiler_spec.rb
CHANGED
@@ -66,6 +66,16 @@ describe Attributor::DSLCompiler do
|
|
66
66
|
it 'accepts explicit nil type' do
|
67
67
|
dsl_compiler.attribute(attribute_name, nil, attribute_options)
|
68
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
|
78
|
+
end
|
69
79
|
end
|
70
80
|
|
71
81
|
context 'for a referenced Model attribute' 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
|
data/spec/types/hash_spec.rb
CHANGED
@@ -497,6 +497,22 @@ describe Attributor::Hash do
|
|
497
497
|
end
|
498
498
|
end
|
499
499
|
|
500
|
+
context '.requirements' do
|
501
|
+
let(:type) { Attributor::Hash.construct(block) }
|
502
|
+
|
503
|
+
context 'forces processing of lazy key initialization' do
|
504
|
+
let(:block) do
|
505
|
+
proc do
|
506
|
+
key 'name', String
|
507
|
+
requires 'name'
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
it 'lists the requirements' do
|
512
|
+
expect(type.requirements).to_not be_empty
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
500
516
|
context '#validate' do
|
501
517
|
context 'for a key and value typed hash' do
|
502
518
|
let(:key_type) { Integer }
|
@@ -651,7 +667,7 @@ describe Attributor::Hash do
|
|
651
667
|
at_least(1).of 'consistency', 'availability', 'partitioning'
|
652
668
|
end
|
653
669
|
# Silly example, just to show that block and inline requires can be combined
|
654
|
-
requires.at_most(
|
670
|
+
requires.at_most(1).of 'consistency', 'availability', 'partitioning'
|
655
671
|
end
|
656
672
|
end
|
657
673
|
|
@@ -663,6 +679,37 @@ describe Attributor::Hash do
|
|
663
679
|
)
|
664
680
|
end
|
665
681
|
end
|
682
|
+
context 'using a combo of things to test example gen' do
|
683
|
+
let(:block) do
|
684
|
+
proc do
|
685
|
+
key :req1, String
|
686
|
+
key :req2, String
|
687
|
+
key :exc3, String
|
688
|
+
key :exc4, String
|
689
|
+
key :least1, String
|
690
|
+
key :least2, String
|
691
|
+
key :exact1, String
|
692
|
+
key :exact2, String
|
693
|
+
key :most1, String
|
694
|
+
key :most2, String
|
695
|
+
|
696
|
+
requires.all :req1, :req2
|
697
|
+
requires.exclusive :exc3, :exc4
|
698
|
+
requires.at_least(2).of :least1, :least2
|
699
|
+
requires.exactly(1).of :exc3, :exact1, :exact2
|
700
|
+
requires.at_most(1).of :most1, :most2
|
701
|
+
requires.at_least(1).of :exc4, :exc3
|
702
|
+
end
|
703
|
+
end
|
704
|
+
it 'comes up with a reasonably good set' do
|
705
|
+
ex = type.example
|
706
|
+
expect(ex.keys).to match([:req1, :req2, :exc3, :least1, :least2, :most1])
|
707
|
+
end
|
708
|
+
it 'it favors picking attributes with data' do
|
709
|
+
ex = type.example(nil,{most2: "data"})
|
710
|
+
expect(ex.keys).to match([:req1, :req2, :exc3, :least1, :least2, :most2])
|
711
|
+
end
|
712
|
+
end
|
666
713
|
end
|
667
714
|
end
|
668
715
|
|
@@ -687,6 +734,13 @@ describe Attributor::Hash do
|
|
687
734
|
expect(description[:name]).to eq('Hash')
|
688
735
|
expect(description[:key]).to eq(type: { name: 'Object', id: 'Attributor-Object', family: 'any' })
|
689
736
|
expect(description[:value]).to eq(type: { name: 'Object', id: 'Attributor-Object', family: 'any' })
|
737
|
+
expect(description).to_not have_key(:example)
|
738
|
+
end
|
739
|
+
context 'when there is a given example' do
|
740
|
+
let(:example) { { 'one' => 1, two: 2 } }
|
741
|
+
it 'uses it, even though there are not individual keys' do
|
742
|
+
expect(description[:example]).to eq(example)
|
743
|
+
end
|
690
744
|
end
|
691
745
|
end
|
692
746
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: attributor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 5.
|
4
|
+
version: 5.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:
|
12
|
+
date: 2017-12-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: hashie
|
@@ -99,16 +99,16 @@ dependencies:
|
|
99
99
|
name: yard
|
100
100
|
requirement: !ruby/object:Gem::Requirement
|
101
101
|
requirements:
|
102
|
-
- - "
|
102
|
+
- - ">="
|
103
103
|
- !ruby/object:Gem::Version
|
104
|
-
version: 0
|
104
|
+
version: '0'
|
105
105
|
type: :development
|
106
106
|
prerelease: false
|
107
107
|
version_requirements: !ruby/object:Gem::Requirement
|
108
108
|
requirements:
|
109
|
-
- - "
|
109
|
+
- - ">="
|
110
110
|
- !ruby/object:Gem::Version
|
111
|
-
version: 0
|
111
|
+
version: '0'
|
112
112
|
- !ruby/object:Gem::Dependency
|
113
113
|
name: backports
|
114
114
|
requirement: !ruby/object:Gem::Requirement
|
@@ -338,6 +338,7 @@ files:
|
|
338
338
|
- lib/attributor/families/numeric.rb
|
339
339
|
- lib/attributor/families/temporal.rb
|
340
340
|
- lib/attributor/hash_dsl_compiler.rb
|
341
|
+
- lib/attributor/smart_attribute_selector.rb
|
341
342
|
- lib/attributor/type.rb
|
342
343
|
- lib/attributor/types/bigdecimal.rb
|
343
344
|
- lib/attributor/types/boolean.rb
|
@@ -371,6 +372,7 @@ files:
|
|
371
372
|
- spec/extras/field_selector/field_selector_spec.rb
|
372
373
|
- spec/families_spec.rb
|
373
374
|
- spec/hash_dsl_compiler_spec.rb
|
375
|
+
- spec/smart_attribute_selector_spec.rb
|
374
376
|
- spec/spec_helper.rb
|
375
377
|
- spec/support/hashes.rb
|
376
378
|
- spec/support/models.rb
|
@@ -419,7 +421,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
419
421
|
version: '0'
|
420
422
|
requirements: []
|
421
423
|
rubyforge_project:
|
422
|
-
rubygems_version: 2.
|
424
|
+
rubygems_version: 2.6.12
|
423
425
|
signing_key:
|
424
426
|
specification_version: 4
|
425
427
|
summary: A powerful attribute and type management library for Ruby
|
@@ -432,6 +434,7 @@ test_files:
|
|
432
434
|
- spec/extras/field_selector/field_selector_spec.rb
|
433
435
|
- spec/families_spec.rb
|
434
436
|
- spec/hash_dsl_compiler_spec.rb
|
437
|
+
- spec/smart_attribute_selector_spec.rb
|
435
438
|
- spec/spec_helper.rb
|
436
439
|
- spec/support/hashes.rb
|
437
440
|
- spec/support/models.rb
|
@@ -460,4 +463,3 @@ test_files:
|
|
460
463
|
- spec/types/time_spec.rb
|
461
464
|
- spec/types/type_spec.rb
|
462
465
|
- spec/types/uri_spec.rb
|
463
|
-
has_rdoc:
|