attributor 5.0.2 → 5.1.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/.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
|