attributor 5.1.0 → 5.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 +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:
|