attributor 5.0.2 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +30 -0
- data/.travis.yml +6 -4
- data/CHANGELOG.md +6 -1
- data/Gemfile +1 -1
- data/Guardfile +14 -8
- data/Rakefile +4 -5
- data/attributor.gemspec +34 -29
- data/lib/attributor.rb +23 -29
- data/lib/attributor/attribute.rb +108 -127
- data/lib/attributor/attribute_resolver.rb +12 -26
- data/lib/attributor/dsl_compiler.rb +17 -21
- data/lib/attributor/dumpable.rb +1 -2
- data/lib/attributor/example_mixin.rb +5 -8
- data/lib/attributor/exceptions.rb +5 -6
- data/lib/attributor/extensions/randexp.rb +3 -5
- data/lib/attributor/extras/field_selector.rb +4 -4
- data/lib/attributor/extras/field_selector/transformer.rb +6 -7
- data/lib/attributor/families/numeric.rb +0 -2
- data/lib/attributor/families/temporal.rb +1 -4
- data/lib/attributor/hash_dsl_compiler.rb +22 -25
- data/lib/attributor/type.rb +24 -32
- data/lib/attributor/types/bigdecimal.rb +7 -14
- data/lib/attributor/types/boolean.rb +5 -8
- data/lib/attributor/types/class.rb +9 -10
- data/lib/attributor/types/collection.rb +34 -44
- data/lib/attributor/types/container.rb +9 -15
- data/lib/attributor/types/csv.rb +7 -10
- data/lib/attributor/types/date.rb +20 -25
- data/lib/attributor/types/date_time.rb +7 -14
- data/lib/attributor/types/float.rb +4 -6
- data/lib/attributor/types/hash.rb +171 -196
- data/lib/attributor/types/ids.rb +2 -6
- data/lib/attributor/types/integer.rb +12 -17
- data/lib/attributor/types/model.rb +39 -48
- data/lib/attributor/types/object.rb +2 -4
- data/lib/attributor/types/polymorphic.rb +118 -0
- data/lib/attributor/types/regexp.rb +4 -5
- data/lib/attributor/types/string.rb +6 -7
- data/lib/attributor/types/struct.rb +8 -15
- data/lib/attributor/types/symbol.rb +3 -6
- data/lib/attributor/types/tempfile.rb +5 -6
- data/lib/attributor/types/time.rb +11 -11
- data/lib/attributor/types/uri.rb +9 -10
- data/lib/attributor/version.rb +1 -1
- data/spec/attribute_resolver_spec.rb +57 -78
- data/spec/attribute_spec.rb +174 -216
- data/spec/attributor_spec.rb +11 -15
- data/spec/dsl_compiler_spec.rb +19 -33
- data/spec/dumpable_spec.rb +6 -7
- data/spec/extras/field_selector/field_selector_spec.rb +1 -1
- data/spec/families_spec.rb +1 -3
- data/spec/hash_dsl_compiler_spec.rb +65 -74
- data/spec/spec_helper.rb +9 -3
- data/spec/support/hashes.rb +2 -3
- data/spec/support/models.rb +30 -36
- data/spec/support/polymorphics.rb +10 -0
- data/spec/type_spec.rb +38 -61
- data/spec/types/bigdecimal_spec.rb +11 -15
- data/spec/types/boolean_spec.rb +12 -39
- data/spec/types/class_spec.rb +10 -11
- data/spec/types/collection_spec.rb +72 -81
- data/spec/types/container_spec.rb +22 -26
- data/spec/types/csv_spec.rb +15 -16
- data/spec/types/date_spec.rb +16 -33
- data/spec/types/date_time_spec.rb +16 -33
- data/spec/types/file_upload_spec.rb +1 -2
- data/spec/types/float_spec.rb +7 -14
- data/spec/types/hash_spec.rb +285 -289
- data/spec/types/ids_spec.rb +5 -7
- data/spec/types/integer_spec.rb +37 -46
- data/spec/types/model_spec.rb +111 -128
- data/spec/types/polymorphic_spec.rb +134 -0
- data/spec/types/regexp_spec.rb +4 -7
- data/spec/types/string_spec.rb +17 -21
- data/spec/types/struct_spec.rb +40 -47
- data/spec/types/tempfile_spec.rb +1 -2
- data/spec/types/temporal_spec.rb +9 -0
- data/spec/types/time_spec.rb +16 -32
- data/spec/types/type_spec.rb +15 -0
- data/spec/types/uri_spec.rb +6 -7
- metadata +77 -25
data/spec/attribute_spec.rb
CHANGED
@@ -1,74 +1,69 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), 'spec_helper.rb')
|
2
2
|
|
3
|
-
|
4
3
|
describe Attributor::Attribute do
|
5
|
-
|
6
4
|
let(:attribute_options) { Hash.new }
|
7
5
|
let(:type) { Attributor::String }
|
8
6
|
|
9
7
|
subject(:attribute) { Attributor::Attribute.new(type, attribute_options) }
|
10
8
|
|
11
|
-
let(:context) { [
|
12
|
-
let(:value) {
|
9
|
+
let(:context) { ['context'] }
|
10
|
+
let(:value) { 'one' }
|
13
11
|
|
14
12
|
context 'initialize' do
|
15
13
|
its(:type) { should be type }
|
16
14
|
its(:options) { should be attribute_options }
|
17
15
|
|
18
16
|
it 'calls check_options!' do
|
19
|
-
Attributor::Attribute.
|
17
|
+
expect_any_instance_of(Attributor::Attribute).to receive(:check_options!)
|
20
18
|
Attributor::Attribute.new(type, attribute_options)
|
21
19
|
end
|
22
20
|
|
23
21
|
context 'for anonymous types (aka Structs)' do
|
24
22
|
before do
|
25
|
-
Attributor.
|
23
|
+
expect(Attributor).to receive(:resolve_type).once.with(Struct, attribute_options, anything).and_call_original
|
26
24
|
end
|
27
25
|
|
28
26
|
it 'generates the class' do
|
29
|
-
|
27
|
+
Attributor::Attribute.new(Struct, attribute_options) do
|
30
28
|
attribute :id, Integer
|
31
29
|
end
|
32
30
|
end
|
33
|
-
|
34
31
|
end
|
35
|
-
|
36
32
|
end
|
37
33
|
|
38
34
|
context '==' do
|
39
35
|
let(:other_attribute) { Attributor::Attribute.new(type, attribute_options) }
|
40
|
-
it { should
|
36
|
+
it { should eq other_attribute }
|
41
37
|
end
|
42
38
|
|
43
39
|
context 'describe' do
|
44
|
-
let(:attribute_options) { {:
|
40
|
+
let(:attribute_options) { { required: true, values: ['one'], description: 'something', min: 0 } }
|
45
41
|
let(:expected) do
|
46
|
-
h = {type: {name: 'String', id: type.id, family: type.family}}
|
47
|
-
common = attribute_options.select{|k,
|
48
|
-
h.merge!(
|
49
|
-
h[:options] = {:
|
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 }
|
50
46
|
h
|
51
47
|
end
|
52
48
|
|
53
|
-
|
54
|
-
its(:describe) { should == expected }
|
49
|
+
its(:describe) { should eq expected }
|
55
50
|
|
56
51
|
context 'with example options' do
|
57
|
-
let(:attribute_options) { {:
|
52
|
+
let(:attribute_options) { { description: 'something', example: 'ex_def' } }
|
58
53
|
its(:describe) { should have_key(:example_definition) }
|
59
54
|
its(:describe) { should_not have_key(:example) }
|
60
55
|
it 'should have the example value in the :example_definition key' do
|
61
|
-
subject.describe[:example_definition].
|
56
|
+
expect(subject.describe[:example_definition]).to eq 'ex_def'
|
62
57
|
end
|
63
58
|
end
|
64
59
|
|
65
60
|
context 'with custom_data' do
|
66
|
-
let(:custom_data) { {loggable: true, visible_in_ui: false} }
|
67
|
-
let(:attribute_options) { {custom_data: custom_data} }
|
61
|
+
let(:custom_data) { { loggable: true, visible_in_ui: false } }
|
62
|
+
let(:attribute_options) { { custom_data: custom_data } }
|
68
63
|
its(:describe) { should have_key(:custom_data) }
|
69
64
|
|
70
65
|
it 'keep the custom data attribute' do
|
71
|
-
subject.describe[:custom_data].
|
66
|
+
expect(subject.describe[:custom_data]).to eq custom_data
|
72
67
|
end
|
73
68
|
end
|
74
69
|
|
@@ -80,99 +75,92 @@ describe Attributor::Attribute do
|
|
80
75
|
end
|
81
76
|
end
|
82
77
|
|
83
|
-
|
84
78
|
subject(:description) { attribute.describe }
|
85
79
|
|
86
|
-
|
87
80
|
it 'uses the name of the first non-anonymous ancestor' do
|
88
|
-
description[:type][:name].
|
81
|
+
expect(description[:type][:name]).to eq 'Struct'
|
89
82
|
end
|
90
83
|
|
91
84
|
it 'includes sub-attributes' do
|
92
|
-
description[:type][:attributes].
|
85
|
+
expect(description[:type][:attributes]).to have_key(:id)
|
93
86
|
end
|
94
|
-
|
95
87
|
end
|
96
88
|
|
97
89
|
context 'with an example' do
|
98
|
-
|
99
|
-
let(:
|
100
|
-
|
101
|
-
subject(:described){ attribute.describe(false, example: example) }
|
90
|
+
let(:attribute_options) { {} }
|
91
|
+
let(:example) { attribute.example }
|
92
|
+
subject(:described) { attribute.describe(false, example: example) }
|
102
93
|
|
103
94
|
context 'using a simple terminal type' do
|
104
95
|
let(:type) { String }
|
105
|
-
its(:keys){ should include(:example) }
|
96
|
+
its(:keys) { should include(:example) }
|
106
97
|
it 'should have the passed example value' do
|
107
|
-
described.
|
108
|
-
described[:example].
|
98
|
+
expect(described).to have_key(:example)
|
99
|
+
expect(described[:example]).to eq(example)
|
109
100
|
end
|
110
101
|
it 'should have removed the example from the :type' do
|
111
|
-
described[:type].
|
102
|
+
expect(described[:type]).not_to have_key(:example)
|
112
103
|
end
|
113
|
-
|
114
104
|
end
|
115
105
|
|
116
106
|
context 'using a complex type' do
|
117
107
|
let(:type) { Cormorant }
|
118
|
-
its(:keys){ should_not include(:example) }
|
108
|
+
its(:keys) { should_not include(:example) }
|
119
109
|
|
120
110
|
it 'Should see examples in the right places, depending on leaf/no-leaf types' do
|
121
111
|
# String, a leaf attribute type: should have example
|
122
112
|
name_attr = described[:type][:attributes][:name]
|
123
|
-
name_attr.
|
124
|
-
name_attr[:type].
|
113
|
+
expect(name_attr).to include(:example)
|
114
|
+
expect(name_attr[:type]).not_to include(:example)
|
125
115
|
|
126
116
|
# Struct, a non-leaf attribute type: shouldn't have example
|
127
|
-
ts_attr =
|
128
|
-
ts_attr.
|
129
|
-
ts_attr[:type].
|
117
|
+
ts_attr = described[:type][:attributes][:timestamps]
|
118
|
+
expect(ts_attr).not_to include(:example)
|
119
|
+
expect(ts_attr[:type]).not_to include(:example)
|
130
120
|
|
131
121
|
# DateTime inside a Struct, a nested leaf attribute type: should have example
|
132
|
-
born_attr =
|
133
|
-
born_attr.
|
134
|
-
born_attr[:type].
|
122
|
+
born_attr = ts_attr[:type][:attributes][:born_at]
|
123
|
+
expect(born_attr).to include(:example)
|
124
|
+
expect(born_attr[:type]).not_to include(:example)
|
135
125
|
end
|
136
126
|
end
|
137
127
|
end
|
138
128
|
end
|
139
129
|
|
140
|
-
|
141
130
|
context 'parse' do
|
142
|
-
let(:loaded_object){ double("I'm loaded") }
|
131
|
+
let(:loaded_object) { double("I'm loaded") }
|
143
132
|
it 'loads and validates' do
|
144
|
-
attribute.
|
145
|
-
attribute.
|
133
|
+
expect(attribute).to receive(:load).with(value, Attributor::DEFAULT_ROOT_CONTEXT).and_return(loaded_object)
|
134
|
+
expect(attribute).to receive(:validate).with(loaded_object, Attributor::DEFAULT_ROOT_CONTEXT).and_call_original
|
146
135
|
|
147
136
|
attribute.parse(value)
|
148
137
|
end
|
149
138
|
end
|
150
139
|
|
151
|
-
|
152
140
|
context 'checking options' do
|
153
141
|
it 'raises for invalid options' do
|
154
|
-
expect
|
142
|
+
expect do
|
155
143
|
Attributor::Attribute.new(Integer, unknown_opt: true)
|
156
|
-
|
144
|
+
end.to raise_error(/unsupported option/)
|
157
145
|
end
|
158
146
|
|
159
147
|
it 'has a spec that we try to validate the :default value' do
|
160
|
-
expect
|
161
|
-
Attributor::Attribute.new(Integer, default:
|
162
|
-
|
148
|
+
expect do
|
149
|
+
Attributor::Attribute.new(Integer, default: 'not an okay integer')
|
150
|
+
end.to raise_error(/Default value doesn't have the correct attribute type/)
|
163
151
|
end
|
164
152
|
|
165
153
|
context 'custom_data' do
|
166
154
|
it 'raises when not a hash' do
|
167
|
-
expect
|
155
|
+
expect do
|
168
156
|
Attributor::Attribute.new(Integer, custom_data: 1)
|
169
|
-
|
157
|
+
end.to raise_error(/custom_data must be a Hash/)
|
170
158
|
end
|
171
159
|
|
172
160
|
it 'does not raise for hashes' do
|
173
|
-
expect
|
174
|
-
Attributor::Attribute.new(Integer, custom_data: {loggable: true})
|
175
|
-
|
161
|
+
expect do
|
162
|
+
Attributor::Attribute.new(Integer, custom_data: { loggable: true })
|
163
|
+
end.not_to raise_error
|
176
164
|
end
|
177
165
|
end
|
178
166
|
end
|
@@ -183,54 +171,53 @@ describe Attributor::Attribute do
|
|
183
171
|
context 'with nothing specified' do
|
184
172
|
let(:attribute_options) { {} }
|
185
173
|
before do
|
186
|
-
type.
|
174
|
+
expect(type).to receive(:example).and_return(example)
|
187
175
|
end
|
188
176
|
|
189
177
|
it 'defers to the type' do
|
190
|
-
attribute.example.
|
178
|
+
expect(attribute.example).to be example
|
191
179
|
end
|
192
180
|
end
|
193
181
|
|
194
182
|
context 'with an attribute that has the values option set' do
|
195
|
-
let(:values) {
|
196
|
-
let(:attribute_options) { {:values
|
183
|
+
let(:values) { %w(one two) }
|
184
|
+
let(:attribute_options) { { values: values } }
|
197
185
|
it 'picks a random value' do
|
198
|
-
values.
|
186
|
+
expect(values).to include subject.example
|
199
187
|
end
|
200
|
-
|
201
188
|
end
|
202
189
|
|
203
190
|
context 'deterministic examples' do
|
204
191
|
let(:example) { /\w+/ }
|
205
|
-
let(:attribute_options) { {:example
|
192
|
+
let(:attribute_options) { { example: example } }
|
206
193
|
|
207
194
|
it 'can take a context to pre-seed the random number generator' do
|
208
195
|
example_1 = subject.example(['context'])
|
209
196
|
example_2 = subject.example(['context'])
|
210
197
|
|
211
|
-
example_1.
|
198
|
+
expect(example_1).to eq example_2
|
212
199
|
end
|
213
200
|
|
214
201
|
it 'can take a context to pre-seed the random number generator' do
|
215
202
|
example_1 = subject.example(['context'])
|
216
203
|
example_2 = subject.example(['different context'])
|
217
204
|
|
218
|
-
example_1.
|
205
|
+
expect(example_1).not_to eq example_2
|
219
206
|
end
|
220
207
|
end
|
221
208
|
|
222
209
|
context 'with an example option' do
|
223
|
-
let(:example){
|
224
|
-
let(:attribute_options) { {example: example
|
210
|
+
let(:example) { 'Bob' }
|
211
|
+
let(:attribute_options) { { example: example, regexp: /Bob/ } }
|
225
212
|
|
226
|
-
its(:example){ should
|
213
|
+
its(:example) { should eq example }
|
227
214
|
|
228
215
|
context 'that is not valid' do
|
229
|
-
let(:example){
|
216
|
+
let(:example) { 'Frank' }
|
230
217
|
it 'raises a validation error' do
|
231
|
-
expect
|
218
|
+
expect do
|
232
219
|
subject.example
|
233
|
-
|
220
|
+
end.to raise_error(Attributor::AttributorException, /Error generating example/)
|
234
221
|
end
|
235
222
|
end
|
236
223
|
end
|
@@ -239,17 +226,17 @@ describe Attributor::Attribute do
|
|
239
226
|
context 'example_from_options' do
|
240
227
|
let(:example) { nil }
|
241
228
|
let(:generated_example) { example }
|
242
|
-
let(:attribute_options) { {:example
|
243
|
-
let(:parent){ nil }
|
244
|
-
let(:context){ Attributor::DEFAULT_ROOT_CONTEXT}
|
229
|
+
let(:attribute_options) { { example: example } }
|
230
|
+
let(:parent) { nil }
|
231
|
+
let(:context) { Attributor::DEFAULT_ROOT_CONTEXT }
|
245
232
|
|
246
|
-
subject(:example_result) { attribute.example_from_options(
|
233
|
+
subject(:example_result) { attribute.example_from_options(parent, context) }
|
247
234
|
before do
|
248
|
-
attribute.
|
235
|
+
expect(attribute).to receive(:load).with(generated_example, an_instance_of(Array)).and_call_original
|
249
236
|
end
|
250
237
|
|
251
238
|
context 'with a string' do
|
252
|
-
let(:example) {
|
239
|
+
let(:example) { 'example' }
|
253
240
|
|
254
241
|
it { should be example }
|
255
242
|
end
|
@@ -265,9 +252,9 @@ describe Attributor::Attribute do
|
|
265
252
|
let(:generated_example) { /\w+/.gen }
|
266
253
|
|
267
254
|
it 'calls #gen on the regexp' do
|
268
|
-
example.
|
255
|
+
expect(example).to receive(:gen).and_return(generated_example)
|
269
256
|
|
270
|
-
example_result.
|
257
|
+
expect(example_result).to match example
|
271
258
|
end
|
272
259
|
|
273
260
|
context 'for a type with a non-String native_type' do
|
@@ -276,113 +263,109 @@ describe Attributor::Attribute do
|
|
276
263
|
let(:generated_example) { /\d{5}/.gen }
|
277
264
|
|
278
265
|
it 'coerces the example value properly' do
|
279
|
-
example.
|
280
|
-
type.
|
266
|
+
expect(example).to receive(:gen).and_return(generated_example)
|
267
|
+
expect(type).to receive(:load).and_call_original
|
281
268
|
|
282
|
-
example_result.
|
269
|
+
expect(example_result).to be_kind_of(type.native_type)
|
283
270
|
end
|
284
271
|
end
|
285
|
-
|
286
272
|
end
|
287
273
|
|
288
274
|
context 'with a proc' do
|
289
|
-
let(:parent){ Object.new }
|
275
|
+
let(:parent) { Object.new }
|
290
276
|
|
291
277
|
context 'with one argument' do
|
292
|
-
let(:example) {
|
278
|
+
let(:example) { ->(_obj) { 'ok' } }
|
293
279
|
let(:generated_example) { 'ok' }
|
294
280
|
|
295
281
|
before do
|
296
|
-
example.
|
282
|
+
expect(example).to receive(:call).with(parent).and_return(generated_example)
|
297
283
|
end
|
298
284
|
|
299
285
|
it 'passes any given parent through to the example proc' do
|
300
|
-
example_result.
|
286
|
+
expect(example_result).to eq 'ok'
|
301
287
|
end
|
302
288
|
end
|
303
289
|
|
304
290
|
context 'with two arguments' do
|
305
|
-
let(:example) {
|
291
|
+
let(:example) { ->(_obj, context) { "#{context} ok" } }
|
306
292
|
let(:generated_example) { "#{context} ok" }
|
307
|
-
let(:context){ ['some_context'] }
|
293
|
+
let(:context) { ['some_context'] }
|
308
294
|
before do
|
309
|
-
example.
|
295
|
+
expect(example).to receive(:call).with(parent, context).and_return(generated_example)
|
310
296
|
end
|
311
297
|
|
312
298
|
it 'passes any given parent through to the example proc' do
|
313
|
-
example_result.
|
299
|
+
expect(example_result).to eq "#{context} ok"
|
314
300
|
end
|
315
301
|
end
|
316
|
-
|
317
302
|
end
|
318
303
|
|
319
304
|
context 'with an Collection (of Strings)' do
|
320
305
|
let(:type) { Attributor::Collection.of(String) }
|
321
|
-
let(:example) { [
|
322
|
-
it { should
|
306
|
+
let(:example) { ['one'] }
|
307
|
+
it { should eq example }
|
323
308
|
end
|
324
|
-
|
325
309
|
end
|
326
310
|
|
327
311
|
context 'load' do
|
328
|
-
let(:context){ ['context'] }
|
312
|
+
let(:context) { ['context'] }
|
329
313
|
let(:value) { '1' }
|
330
314
|
|
331
315
|
it 'delegates to type.load' do
|
332
|
-
type.
|
333
|
-
attribute.load(value,context)
|
316
|
+
expect(type).to receive(:load).with(value, context, {})
|
317
|
+
attribute.load(value, context)
|
334
318
|
end
|
335
319
|
|
336
320
|
it 'passes options to type.load' do
|
337
|
-
type.
|
321
|
+
expect(type).to receive(:load).with(value, context, foo: 'bar')
|
338
322
|
attribute.load(value, context, foo: 'bar')
|
339
323
|
end
|
340
324
|
|
341
325
|
context 'applying default values' do
|
342
326
|
let(:value) { nil }
|
343
|
-
let(:default_value) {
|
344
|
-
let(:attribute_options) { {:
|
327
|
+
let(:default_value) { 'default value' }
|
328
|
+
let(:attribute_options) { { default: default_value } }
|
345
329
|
|
346
330
|
subject(:result) { attribute.load(value) }
|
347
331
|
|
348
332
|
context 'for nil' do
|
349
|
-
it { should
|
333
|
+
it { should eq default_value }
|
350
334
|
end
|
351
335
|
|
352
336
|
context 'for false' do
|
353
337
|
let(:type) { Attributor::Boolean }
|
354
338
|
let(:default_value) { false }
|
355
|
-
it { should
|
356
|
-
|
339
|
+
it { should eq default_value }
|
357
340
|
end
|
358
341
|
|
359
342
|
context 'for a Proc-based default value' do
|
360
|
-
let(:context){ [
|
361
|
-
subject(:result){ attribute.load(value,context) }
|
362
|
-
|
343
|
+
let(:context) { ['$'] }
|
344
|
+
subject(:result) { attribute.load(value, context) }
|
363
345
|
|
364
346
|
context 'with no arguments arguments' do
|
365
|
-
let(:default_value) { proc {
|
366
|
-
it { should
|
347
|
+
let(:default_value) { proc { 'no_params' } }
|
348
|
+
it { should eq default_value.call }
|
367
349
|
end
|
368
350
|
|
369
351
|
context 'with 1 argument (the parent)' do
|
370
|
-
let(:default_value) { proc {|parent| "parent is fake: #{parent.class}" } }
|
371
|
-
it { should
|
352
|
+
let(:default_value) { proc { |parent| "parent is fake: #{parent.class}" } }
|
353
|
+
it { should eq 'parent is fake: Attributor::FakeParent' }
|
372
354
|
end
|
373
355
|
|
374
356
|
context 'with 2 argument (the parent and the contents)' do
|
375
|
-
let(:default_value) { proc {|parent,context| "parent is fake: #{parent.class} and context is: #{context}" } }
|
376
|
-
it { should
|
357
|
+
let(:default_value) { proc { |parent, context| "parent is fake: #{parent.class} and context is: #{context}" } }
|
358
|
+
it { should eq 'parent is fake: Attributor::FakeParent and context is: ["$"]' }
|
377
359
|
end
|
378
360
|
|
379
361
|
context 'which attempts to use the parent (which is not supported for the moment)' do
|
380
|
-
let(:default_value) { proc {|parent| "any parent method should spit out warning: [#{parent.something}]" } }
|
381
|
-
it
|
362
|
+
let(:default_value) { proc { |parent| "any parent method should spit out warning: [#{parent.something}]" } }
|
363
|
+
it 'should output a warning' do
|
382
364
|
begin
|
383
|
-
old_verbose
|
384
|
-
|
385
|
-
|
365
|
+
old_verbose = $VERBOSE
|
366
|
+
$VERBOSE = nil
|
367
|
+
expect(Kernel).to receive(:warn).and_call_original
|
368
|
+
expect(attribute.load(value, context)).to eq 'any parent method should spit out warning: []'
|
386
369
|
ensure
|
387
370
|
$VERBOSE = old_verbose
|
388
371
|
end
|
@@ -392,61 +375,56 @@ describe Attributor::Attribute do
|
|
392
375
|
end
|
393
376
|
|
394
377
|
context 'validating a value' do
|
395
|
-
|
396
378
|
context '#validate' do
|
397
379
|
context 'applying attribute options' do
|
398
380
|
context ':required' do
|
399
|
-
let(:attribute_options) { {:
|
381
|
+
let(:attribute_options) { { required: true } }
|
400
382
|
context 'with a nil value' do
|
401
383
|
let(:value) { nil }
|
402
384
|
it 'returns an error' do
|
403
|
-
attribute.validate(value, context).first.
|
385
|
+
expect(attribute.validate(value, context).first).to eq 'Attribute context is required'
|
404
386
|
end
|
405
387
|
end
|
406
388
|
end
|
407
389
|
|
408
390
|
context ':values' do
|
409
|
-
let(:values) {
|
410
|
-
let(:attribute_options) { {:values
|
391
|
+
let(:values) { %w(one two) }
|
392
|
+
let(:attribute_options) { { values: values } }
|
411
393
|
let(:value) { nil }
|
412
394
|
|
413
|
-
subject(:errors) { attribute.validate(value, context)}
|
395
|
+
subject(:errors) { attribute.validate(value, context) }
|
414
396
|
|
415
397
|
context 'with a value that is allowed' do
|
416
|
-
let(:value) {
|
398
|
+
let(:value) { 'one' }
|
417
399
|
it 'returns no errors' do
|
418
|
-
errors.
|
400
|
+
expect(errors).to be_empty
|
419
401
|
end
|
420
402
|
end
|
421
403
|
|
422
404
|
context 'with a value that is not allowed' do
|
423
|
-
let(:value) {
|
405
|
+
let(:value) { 'three' }
|
424
406
|
it 'returns an error indicating the problem' do
|
425
|
-
errors.first.
|
407
|
+
expect(errors.first).to match(/is not within the allowed values/)
|
426
408
|
end
|
427
|
-
|
428
409
|
end
|
429
410
|
end
|
430
|
-
|
431
|
-
|
432
411
|
end
|
433
412
|
|
434
413
|
it 'calls the right validate_X methods?' do
|
435
|
-
attribute.
|
436
|
-
attribute.
|
437
|
-
type.
|
414
|
+
expect(attribute).to receive(:validate_type).with(value, context).and_call_original
|
415
|
+
expect(attribute).not_to receive(:validate_dependency)
|
416
|
+
expect(type).to receive(:validate).and_call_original
|
438
417
|
attribute.validate(value, context)
|
439
418
|
end
|
440
|
-
|
441
419
|
end
|
442
420
|
|
443
421
|
context '#validate_type' do
|
444
|
-
subject(:errors) { attribute.validate_type(value, context)}
|
422
|
+
subject(:errors) { attribute.validate_type(value, context) }
|
445
423
|
|
446
424
|
context 'with a value of the right type' do
|
447
|
-
let(:value) {
|
425
|
+
let(:value) { 'one' }
|
448
426
|
it 'returns no errors' do
|
449
|
-
errors.
|
427
|
+
expect(errors).to be_empty
|
450
428
|
end
|
451
429
|
end
|
452
430
|
|
@@ -454,30 +432,26 @@ describe Attributor::Attribute do
|
|
454
432
|
let(:value) { 1 }
|
455
433
|
|
456
434
|
it 'returns errors' do
|
457
|
-
errors.
|
458
|
-
errors.first.
|
435
|
+
expect(errors).not_to be_empty
|
436
|
+
expect(errors.first).to match(/is of the wrong type/)
|
459
437
|
end
|
460
|
-
|
461
438
|
end
|
462
|
-
|
463
|
-
|
464
439
|
end
|
465
440
|
|
466
441
|
context '#validate_missing_value' do
|
467
|
-
let(:key) {
|
442
|
+
let(:key) { '$.instance.ssh_key.name' }
|
468
443
|
let(:value) { /\w+/.gen }
|
469
444
|
|
470
|
-
let(:attribute_options) { {:
|
445
|
+
let(:attribute_options) { { required_if: key } }
|
471
446
|
|
472
|
-
let(:ssh_key) { double(
|
473
|
-
let(:instance) { double(
|
447
|
+
let(:ssh_key) { double('ssh_key', name: value) }
|
448
|
+
let(:instance) { double('instance', ssh_key: ssh_key) }
|
474
449
|
|
475
450
|
before { Attributor::AttributeResolver.current.register('instance', instance) }
|
476
451
|
|
477
|
-
let(:attribute_context) { ['$','params','key_material'] }
|
452
|
+
let(:attribute_context) { ['$', 'params', 'key_material'] }
|
478
453
|
subject(:errors) { attribute.validate_missing_value(attribute_context) }
|
479
454
|
|
480
|
-
|
481
455
|
context 'for a simple dependency without a predicate' do
|
482
456
|
context 'that is satisfied' do
|
483
457
|
it { should_not be_empty }
|
@@ -490,36 +464,33 @@ describe Attributor::Attribute do
|
|
490
464
|
end
|
491
465
|
|
492
466
|
context 'with a dependency that has a predicate' do
|
493
|
-
let(:value) {
|
494
|
-
#subject(:errors) { attribute.validate_missing_value('') }
|
467
|
+
let(:value) { 'default_ssh_key_name' }
|
468
|
+
# subject(:errors) { attribute.validate_missing_value('') }
|
495
469
|
|
496
470
|
context 'where the target attribute exists, and matches the predicate' do
|
497
|
-
let(:attribute_options) { {:
|
471
|
+
let(:attribute_options) { { required_if: { key => /default/ } } }
|
498
472
|
|
499
473
|
it { should_not be_empty }
|
500
474
|
|
501
|
-
its(:first) { should
|
475
|
+
its(:first) { should match(/Attribute #{Regexp.quote(Attributor.humanize_context(attribute_context))} is required when #{Regexp.quote(key)} matches/) }
|
502
476
|
end
|
503
477
|
|
504
478
|
context 'where the target attribute exists, but does not match the predicate' do
|
505
|
-
let(:attribute_options) { {:
|
479
|
+
let(:attribute_options) { { required_if: { key => /other/ } } }
|
506
480
|
|
507
481
|
it { should be_empty }
|
508
482
|
end
|
509
483
|
|
510
484
|
context 'where the target attribute does not exist' do
|
511
|
-
let(:attribute_options) { {:
|
512
|
-
let(:ssh_key) { double(
|
485
|
+
let(:attribute_options) { { required_if: { key => /default/ } } }
|
486
|
+
let(:ssh_key) { double('ssh_key', name: nil) }
|
513
487
|
|
514
488
|
it { should be_empty }
|
515
489
|
end
|
516
490
|
end
|
517
|
-
|
518
491
|
end
|
519
|
-
|
520
492
|
end
|
521
493
|
|
522
|
-
|
523
494
|
context 'for an attribute for a subclass of Model' do
|
524
495
|
let(:type) { Chicken }
|
525
496
|
let(:type_options) { Chicken.options }
|
@@ -527,46 +498,43 @@ describe Attributor::Attribute do
|
|
527
498
|
subject(:attribute) { Attributor::Attribute.new(type, attribute_options) }
|
528
499
|
|
529
500
|
it 'has attributes' do
|
530
|
-
attribute.attributes.
|
501
|
+
expect(attribute.attributes).to eq type.attributes
|
531
502
|
end
|
532
503
|
|
533
|
-
#it 'has compiled_definition' do
|
534
|
-
# attribute.compiled_definition.should
|
535
|
-
#end
|
536
|
-
|
504
|
+
# it 'has compiled_definition' do
|
505
|
+
# attribute.compiled_definition.should eq type.definition
|
506
|
+
# end
|
537
507
|
|
538
508
|
it 'merges its options with those of the compiled_definition' do
|
539
|
-
attribute.options.
|
509
|
+
expect(attribute.options).to eq attribute_options.merge(type_options)
|
540
510
|
end
|
541
511
|
|
542
512
|
it 'describe handles sub-attributes nicely' do
|
543
513
|
describe = attribute.describe(false)
|
544
514
|
|
545
|
-
describe[:type][:name].
|
546
|
-
common_options = attribute_options.select{|k,
|
547
|
-
special_options = attribute_options.reject{|k,
|
548
|
-
common_options.each do |k,v|
|
549
|
-
describe[k].
|
515
|
+
expect(describe[:type][:name]).to eq type.name
|
516
|
+
common_options = attribute_options.select { |k, _v| Attributor::Attribute.TOP_LEVEL_OPTIONS.include? k }
|
517
|
+
special_options = attribute_options.reject { |k, _v| Attributor::Attribute.TOP_LEVEL_OPTIONS.include? k }
|
518
|
+
common_options.each do |k, v|
|
519
|
+
expect(describe[k]).to eq v
|
550
520
|
end
|
551
|
-
special_options.each do |k,v|
|
552
|
-
describe[:options][k].
|
521
|
+
special_options.each do |k, v|
|
522
|
+
expect(describe[:options][k]).to eq v
|
553
523
|
end
|
554
|
-
type_options.each do |k,v|
|
555
|
-
describe[:options][k].
|
524
|
+
type_options.each do |k, v|
|
525
|
+
expect(describe[:options][k]).to eq v
|
556
526
|
end
|
557
527
|
|
558
|
-
|
559
|
-
|
560
|
-
describe[:type][:attributes].should have_key(name)
|
528
|
+
attribute.attributes.each do |name, _attr|
|
529
|
+
expect(describe[:type][:attributes]).to have_key(name)
|
561
530
|
end
|
562
|
-
|
563
531
|
end
|
564
532
|
|
565
533
|
it 'supports deterministic examples' do
|
566
|
-
example_1 = attribute.example([
|
567
|
-
example_2 = attribute.example([
|
534
|
+
example_1 = attribute.example(['Chicken context'])
|
535
|
+
example_2 = attribute.example(['Chicken context'])
|
568
536
|
|
569
|
-
example_1.attributes.
|
537
|
+
expect(example_1.attributes).to eq(example_2.attributes)
|
570
538
|
end
|
571
539
|
|
572
540
|
context '#validate' do
|
@@ -575,29 +543,27 @@ describe Attributor::Attribute do
|
|
575
543
|
|
576
544
|
it 'validates sub-attributes' do
|
577
545
|
errors = attribute.validate(chicken)
|
578
|
-
errors.
|
546
|
+
expect(errors).to be_empty
|
579
547
|
end
|
580
548
|
|
581
549
|
context 'with a failing validation' do
|
582
|
-
subject(:chicken) { Chicken.example(age: 150, email:
|
550
|
+
subject(:chicken) { Chicken.example(age: 150, email: 'foo') }
|
583
551
|
let(:email_validation_response) { ["$.email value \(#{chicken.email}\) does not match regexp (/@/)"] }
|
584
552
|
let(:age_validation_response) { ["$.age value \(#{chicken.age}\) is larger than the allowed max (120)"] }
|
585
553
|
|
586
554
|
it 'collects sub-attribute validation errors' do
|
587
555
|
errors = attribute.validate(chicken)
|
588
|
-
errors.
|
556
|
+
expect(errors).to match_array(age_validation_response | email_validation_response)
|
589
557
|
end
|
590
558
|
end
|
591
|
-
|
592
559
|
end
|
593
560
|
|
594
|
-
|
595
561
|
context '#validate_missing_value' do
|
596
562
|
let(:type) { Duck }
|
597
563
|
let(:attribute_name) { nil }
|
598
564
|
let(:attribute) { Duck.attributes[attribute_name] }
|
599
565
|
|
600
|
-
let(:attribute_context) { ['$','duck',
|
566
|
+
let(:attribute_context) { ['$', 'duck', attribute_name.to_s] }
|
601
567
|
subject(:errors) { attribute.validate_missing_value(attribute_context) }
|
602
568
|
|
603
569
|
before do
|
@@ -616,7 +582,7 @@ describe Attributor::Attribute do
|
|
616
582
|
|
617
583
|
context 'where the target attribute exists, and matches the predicate' do
|
618
584
|
it { should_not be_empty }
|
619
|
-
its(:first) { should
|
585
|
+
its(:first) { should eq 'Attribute $.duck.email is required when name (for $.duck) is present.' }
|
620
586
|
end
|
621
587
|
context 'where the target attribute does not exist' do
|
622
588
|
before do
|
@@ -626,7 +592,6 @@ describe Attributor::Attribute do
|
|
626
592
|
end
|
627
593
|
end
|
628
594
|
|
629
|
-
|
630
595
|
context 'for a dependency with a predicate' do
|
631
596
|
let(:attribute_name) { :age }
|
632
597
|
|
@@ -639,7 +604,7 @@ describe Attributor::Attribute do
|
|
639
604
|
|
640
605
|
context 'where the target attribute exists, and matches the predicate' do
|
641
606
|
it { should_not be_empty }
|
642
|
-
its(:first) { should
|
607
|
+
its(:first) { should match(/Attribute #{Regexp.quote('$.duck.age')} is required when name #{Regexp.quote('(for $.duck)')} matches/) }
|
643
608
|
end
|
644
609
|
|
645
610
|
context 'where the target attribute exists, and does not match the predicate' do
|
@@ -655,69 +620,62 @@ describe Attributor::Attribute do
|
|
655
620
|
end
|
656
621
|
it { should be_empty }
|
657
622
|
end
|
658
|
-
|
659
623
|
end
|
660
|
-
|
661
624
|
end
|
662
|
-
|
663
625
|
end
|
664
626
|
end
|
665
627
|
|
666
628
|
context 'for a Collection' do
|
667
629
|
context 'of non-Model (or Struct) type' do
|
668
630
|
let(:member_type) { Attributor::Integer }
|
669
|
-
let(:type) { Attributor::Collection.of(member_type)}
|
670
|
-
let(:member_options) { {:
|
671
|
-
let(:attribute_options) { {:member_options
|
631
|
+
let(:type) { Attributor::Collection.of(member_type) }
|
632
|
+
let(:member_options) { { max: 10 } }
|
633
|
+
let(:attribute_options) { { member_options: member_options } }
|
672
634
|
|
673
635
|
context 'the member_attribute of that type' do
|
674
636
|
subject(:member_attribute) { attribute.type.member_attribute }
|
675
637
|
|
676
|
-
it { should be_kind_of(Attributor::Attribute)}
|
638
|
+
it { should be_kind_of(Attributor::Attribute) }
|
677
639
|
its(:type) { should be(member_type) }
|
678
640
|
its(:options) { should eq(member_options) }
|
679
641
|
end
|
680
642
|
|
681
|
-
context
|
682
|
-
let(:values) { ['1',2,12] }
|
643
|
+
context 'working with members' do
|
644
|
+
let(:values) { ['1', 2, 12] }
|
683
645
|
|
684
646
|
it 'loads' do
|
685
|
-
attribute.load(values).
|
647
|
+
expect(attribute.load(values)).to match_array [1, 2, 12]
|
686
648
|
end
|
687
649
|
|
688
650
|
it 'validates' do
|
689
651
|
object = attribute.load(values)
|
690
652
|
errors = attribute.validate(object)
|
691
653
|
|
692
|
-
errors.
|
693
|
-
errors[0].
|
654
|
+
expect(errors).to have(1).item
|
655
|
+
expect(errors[0]).to match(/value \(12\) is larger/)
|
694
656
|
end
|
695
657
|
end
|
696
|
-
|
697
|
-
|
698
658
|
end
|
699
659
|
|
700
660
|
context 'of a Model (or Struct) type' do
|
701
|
-
subject(:attribute) { Attributor::Attribute.new(type, attribute_options, &attribute_block)
|
661
|
+
subject(:attribute) { Attributor::Attribute.new(type, attribute_options, &attribute_block) }
|
702
662
|
|
703
|
-
let(:attribute_block) {
|
704
|
-
let(:attribute_options) { {reference: Chicken, member_options: member_options} }
|
663
|
+
let(:attribute_block) { proc { attribute :angry, required: true } }
|
664
|
+
let(:attribute_options) { { reference: Chicken, member_options: member_options } }
|
705
665
|
let(:member_type) { Attributor::Struct }
|
706
666
|
let(:type) { Attributor::Collection.of(member_type) }
|
707
667
|
let(:member_options) { {} }
|
708
668
|
|
709
669
|
context 'the member_attribute of that type' do
|
710
670
|
subject(:member_attribute) { attribute.type.member_attribute }
|
711
|
-
it { should be_kind_of(Attributor::Attribute)}
|
671
|
+
it { should be_kind_of(Attributor::Attribute) }
|
712
672
|
its(:options) { should eq(member_options.merge(reference: Chicken, identity: :email)) }
|
713
673
|
its(:attributes) { should have_key :angry }
|
714
674
|
it 'inherited the type and options from the reference' do
|
715
|
-
member_attribute.attributes[:angry].type.
|
716
|
-
member_attribute.attributes[:angry].options.
|
675
|
+
expect(member_attribute.attributes[:angry].type).to be(Chicken.attributes[:angry].type)
|
676
|
+
expect(member_attribute.attributes[:angry].options).to eq(Chicken.attributes[:angry].options.merge(required: true))
|
717
677
|
end
|
718
678
|
end
|
719
|
-
|
720
679
|
end
|
721
680
|
end
|
722
|
-
|
723
681
|
end
|