parametric 0.0.5 → 0.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/.gitignore +1 -0
- data/.travis.yml +1 -2
- data/README.md +596 -163
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/parametric/block_validator.rb +64 -0
- data/lib/parametric/context.rb +44 -0
- data/lib/parametric/default_types.rb +95 -0
- data/lib/parametric/dsl.rb +47 -0
- data/lib/parametric/field.rb +111 -0
- data/lib/parametric/field_dsl.rb +20 -0
- data/lib/parametric/policies.rb +94 -55
- data/lib/parametric/registry.rb +21 -0
- data/lib/parametric/results.rb +13 -0
- data/lib/parametric/schema.rb +151 -0
- data/lib/parametric/version.rb +1 -1
- data/lib/parametric.rb +16 -6
- data/parametric.gemspec +2 -1
- data/spec/dsl_spec.rb +135 -0
- data/spec/field_spec.rb +404 -0
- data/spec/policies_spec.rb +72 -0
- data/spec/schema_spec.rb +253 -0
- data/spec/schema_walk_spec.rb +42 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/validators_spec.rb +97 -0
- metadata +54 -24
- data/lib/parametric/hash.rb +0 -38
- data/lib/parametric/params.rb +0 -86
- data/lib/parametric/typed_params.rb +0 -23
- data/lib/parametric/utils.rb +0 -24
- data/lib/support/class_attribute.rb +0 -68
- data/spec/nested_params_spec.rb +0 -90
- data/spec/parametric_spec.rb +0 -261
data/spec/field_spec.rb
ADDED
@@ -0,0 +1,404 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Parametric::Field do
|
4
|
+
let(:registry) { Parametric.registry }
|
5
|
+
let(:context) { Parametric::Context.new}
|
6
|
+
subject { described_class.new(:a_key, registry) }
|
7
|
+
|
8
|
+
def register_coercion(name, block)
|
9
|
+
registry.policy name do
|
10
|
+
coerce &block
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def resolve(subject, payload)
|
15
|
+
subject.resolve(payload, context)
|
16
|
+
end
|
17
|
+
|
18
|
+
def has_errors
|
19
|
+
expect(context.errors.keys).not_to be_empty
|
20
|
+
end
|
21
|
+
|
22
|
+
def no_errors
|
23
|
+
expect(context.errors.keys).to be_empty
|
24
|
+
end
|
25
|
+
|
26
|
+
def has_error(key, message)
|
27
|
+
expect(context.errors[key]).to include(message)
|
28
|
+
end
|
29
|
+
|
30
|
+
let(:payload) { {a_key: "Joe"} }
|
31
|
+
|
32
|
+
describe "#resolve" do
|
33
|
+
it "returns value" do
|
34
|
+
resolve(subject, payload).tap do |r|
|
35
|
+
expect(r.eligible?).to be true
|
36
|
+
no_errors
|
37
|
+
expect(r.value).to eq "Joe"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
BUILT_IN_COERCIONS = [:string, :integer, :number, :array, :object, :boolean]
|
43
|
+
|
44
|
+
describe "#meta_data" do
|
45
|
+
BUILT_IN_COERCIONS.each do |t|
|
46
|
+
it "policy #{t} adds #{t} to meta data" do
|
47
|
+
subject.policy(t)
|
48
|
+
expect(subject.meta_data[:type]).to eq t
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "#type" do
|
54
|
+
it "is an alias for #policy" do
|
55
|
+
subject.type(:integer)
|
56
|
+
resolve(subject, a_key: "10.0").tap do |r|
|
57
|
+
expect(r.value).to eq 10
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#policy" do
|
63
|
+
it "coerces integer" do
|
64
|
+
subject.policy(:integer)
|
65
|
+
resolve(subject, a_key: "10.0").tap do |r|
|
66
|
+
expect(r.eligible?).to be true
|
67
|
+
no_errors
|
68
|
+
expect(r.value).to eq 10
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
it "coerces number" do
|
73
|
+
subject.policy(:number)
|
74
|
+
resolve(subject, a_key: "10.0").tap do |r|
|
75
|
+
expect(r.eligible?).to be true
|
76
|
+
no_errors
|
77
|
+
expect(r.value).to eq 10.0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it "coerces string" do
|
82
|
+
subject.policy(:string)
|
83
|
+
resolve(subject, a_key: 10.0).tap do |r|
|
84
|
+
expect(r.eligible?).to be true
|
85
|
+
no_errors
|
86
|
+
expect(r.value).to eq "10.0"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#default" do
|
92
|
+
it "is default if missing key" do
|
93
|
+
resolve(subject.default("AA"), foobar: 1).tap do |r|
|
94
|
+
expect(r.eligible?).to be true
|
95
|
+
no_errors
|
96
|
+
expect(r.value).to eq "AA"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
it "returns value if key is present" do
|
101
|
+
resolve(subject.default("AA"), a_key: nil).tap do |r|
|
102
|
+
expect(r.eligible?).to be true
|
103
|
+
no_errors
|
104
|
+
expect(r.value).to eq nil
|
105
|
+
end
|
106
|
+
|
107
|
+
resolve(subject.default("AA"), a_key: "abc").tap do |r|
|
108
|
+
expect(r.eligible?).to be true
|
109
|
+
no_errors
|
110
|
+
expect(r.value).to eq "abc"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "#present" do
|
116
|
+
it "is valid if value is present" do
|
117
|
+
resolve(subject.present, a_key: "abc").tap do |r|
|
118
|
+
expect(r.eligible?).to be true
|
119
|
+
no_errors
|
120
|
+
expect(r.value).to eq "abc"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
it "is invalid if value is empty" do
|
125
|
+
resolve(subject.present, a_key: "").tap do |r|
|
126
|
+
expect(r.eligible?).to be true
|
127
|
+
has_errors
|
128
|
+
expect(r.value).to eq ""
|
129
|
+
end
|
130
|
+
|
131
|
+
resolve(subject.present, a_key: nil).tap do |r|
|
132
|
+
expect(r.eligible?).to be true
|
133
|
+
has_errors
|
134
|
+
expect(r.value).to eq nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
it "is invalid if key is missing" do
|
139
|
+
resolve(subject.present, foo: "abc").tap do |r|
|
140
|
+
expect(r.eligible?).to be true
|
141
|
+
has_errors
|
142
|
+
expect(r.value).to eq nil
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe "#required" do
|
148
|
+
it "is valid if key is present" do
|
149
|
+
resolve(subject.required, a_key: "abc").tap do |r|
|
150
|
+
expect(r.eligible?).to be true
|
151
|
+
no_errors
|
152
|
+
expect(r.value).to eq "abc"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
it "is valid if key is present and value empty" do
|
157
|
+
resolve(subject.required, a_key: "").tap do |r|
|
158
|
+
expect(r.eligible?).to be true
|
159
|
+
no_errors
|
160
|
+
expect(r.value).to eq ""
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
it "is invalid if key is missing" do
|
165
|
+
resolve(subject.required, foobar: "lala").tap do |r|
|
166
|
+
expect(r.eligible?).to be true
|
167
|
+
has_errors
|
168
|
+
expect(r.value).to eq nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
describe "#options" do
|
174
|
+
before do
|
175
|
+
subject.options(['a', 'b', 'c'])
|
176
|
+
end
|
177
|
+
|
178
|
+
it "resolves if value within options" do
|
179
|
+
resolve(subject, a_key: "b").tap do |r|
|
180
|
+
expect(r.eligible?).to be true
|
181
|
+
no_errors
|
182
|
+
expect(r.value).to eq "b"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
it "resolves if value is array within options" do
|
187
|
+
resolve(subject, a_key: ["b", "c"]).tap do |r|
|
188
|
+
expect(r.eligible?).to be true
|
189
|
+
no_errors
|
190
|
+
expect(r.value).to eq ["b", "c"]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
it "does not resolve if missing key" do
|
195
|
+
resolve(subject, foobar: ["b", "c"]).tap do |r|
|
196
|
+
expect(r.eligible?).to be false
|
197
|
+
no_errors
|
198
|
+
expect(r.value).to be_nil
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
it "does resolve if missing key and default set" do
|
203
|
+
subject.default("Foobar")
|
204
|
+
resolve(subject, foobar: ["b", "c"]).tap do |r|
|
205
|
+
expect(r.eligible?).to be true
|
206
|
+
no_errors
|
207
|
+
expect(r.value).to eq "Foobar"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
it "is invalid if missing key and required" do
|
212
|
+
subject = described_class.new(:a_key).required.options(%w(a b c))
|
213
|
+
resolve(subject, foobar: ["b", "c"]).tap do |r|
|
214
|
+
expect(r.eligible?).to be true
|
215
|
+
has_errors
|
216
|
+
expect(r.value).to be_nil
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
it "is invalid if value outside options" do
|
221
|
+
resolve(subject, a_key: "x").tap do |r|
|
222
|
+
expect(r.eligible?).to be true
|
223
|
+
has_errors
|
224
|
+
expect(r.value).to eq "x"
|
225
|
+
end
|
226
|
+
|
227
|
+
resolve(subject, a_key: ["x", "b"]).tap do |r|
|
228
|
+
expect(r.eligible?).to be true
|
229
|
+
has_errors
|
230
|
+
expect(r.value).to eq ["x", "b"]
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
describe ":split policy" do
|
236
|
+
it "splits by comma" do
|
237
|
+
resolve(subject.policy(:split), a_key: "tag1,tag2").tap do |r|
|
238
|
+
expect(r.eligible?).to be true
|
239
|
+
no_errors
|
240
|
+
expect(r.value).to eq ["tag1", "tag2"]
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
describe ":declared policy" do
|
246
|
+
it "is eligible if key exists" do
|
247
|
+
resolve(subject.policy(:declared).present, a_key: "").tap do |r|
|
248
|
+
expect(r.eligible?).to be true
|
249
|
+
has_errors
|
250
|
+
expect(r.value).to eq ""
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
it "is not eligible if key does not exist" do
|
255
|
+
resolve(subject.policy(:declared).present, foo: "").tap do |r|
|
256
|
+
expect(r.eligible?).to be false
|
257
|
+
no_errors
|
258
|
+
expect(r.value).to eq nil
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
describe ":noop policy" do
|
264
|
+
it "does not do anything" do
|
265
|
+
resolve(subject.policy(:noop).present, a_key: "").tap do |r|
|
266
|
+
expect(r.eligible?).to be true
|
267
|
+
has_errors
|
268
|
+
expect(r.value).to eq ""
|
269
|
+
end
|
270
|
+
|
271
|
+
resolve(subject.policy(:noop).present, foo: "").tap do |r|
|
272
|
+
expect(r.eligible?).to be true
|
273
|
+
has_errors
|
274
|
+
expect(r.value).to eq nil
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
describe "#schema" do
|
280
|
+
it "runs sub-schema" do
|
281
|
+
subject.schema do
|
282
|
+
field(:name).policy(:string)
|
283
|
+
field(:tags).policy(:split).policy(:array)
|
284
|
+
end
|
285
|
+
|
286
|
+
payload = {a_key: [{name: "n1", tags: "t1,t2"}, {name: "n2", tags: ["t3"]}]}
|
287
|
+
|
288
|
+
resolve(subject, payload).tap do |r|
|
289
|
+
expect(r.eligible?).to be true
|
290
|
+
no_errors
|
291
|
+
expect(r.value).to eq([
|
292
|
+
{name: "n1", tags: ["t1", "t2"]},
|
293
|
+
{name: "n2", tags: ["t3"]},
|
294
|
+
])
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
describe '#policy' do
|
300
|
+
let(:custom_klass) do
|
301
|
+
Class.new do
|
302
|
+
def initialize(title = 'Sr.')
|
303
|
+
@title = title
|
304
|
+
end
|
305
|
+
|
306
|
+
def eligible?(*_)
|
307
|
+
true
|
308
|
+
end
|
309
|
+
|
310
|
+
def valid?(*_)
|
311
|
+
true
|
312
|
+
end
|
313
|
+
|
314
|
+
def coerce(value, key, context)
|
315
|
+
"#{@title} #{value}"
|
316
|
+
end
|
317
|
+
|
318
|
+
def meta_data
|
319
|
+
{foo: "bar"}
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
it 'works with policy in registry' do
|
325
|
+
register_coercion :foo, ->(v, k, c){ "Hello #{v}" }
|
326
|
+
subject.policy(:foo)
|
327
|
+
resolve(subject, a_key: "Ismael").tap do |r|
|
328
|
+
expect(r.eligible?).to be true
|
329
|
+
no_errors
|
330
|
+
expect(r.value).to eq "Hello Ismael"
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
it 'raises if policy not found' do
|
335
|
+
expect{
|
336
|
+
subject.policy(:foobar)
|
337
|
+
}.to raise_exception Parametric::ConfigurationError
|
338
|
+
end
|
339
|
+
|
340
|
+
it 'chains policies' do
|
341
|
+
registry.policy :general, custom_klass.new("General")
|
342
|
+
registry.policy :commander, custom_klass.new("Commander")
|
343
|
+
|
344
|
+
subject
|
345
|
+
.policy(:general)
|
346
|
+
.policy(:commander)
|
347
|
+
|
348
|
+
resolve(subject, a_key: "Ismael").tap do |r|
|
349
|
+
expect(r.eligible?).to be true
|
350
|
+
no_errors
|
351
|
+
expect(r.value).to eq "Commander General Ismael"
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
it "can instantiate policy class and pass arguments" do
|
356
|
+
registry.policy :job_title, custom_klass
|
357
|
+
|
358
|
+
subject.policy(:job_title, "Developer")
|
359
|
+
|
360
|
+
resolve(subject, a_key: "Ismael").tap do |r|
|
361
|
+
expect(r.eligible?).to be true
|
362
|
+
no_errors
|
363
|
+
expect(r.value).to eq "Developer Ismael"
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
it "can take a class not in the registry" do
|
368
|
+
subject.policy(custom_klass, "Developer")
|
369
|
+
|
370
|
+
resolve(subject, a_key: "Ismael").tap do |r|
|
371
|
+
expect(r.eligible?).to be true
|
372
|
+
no_errors
|
373
|
+
expect(r.value).to eq "Developer Ismael"
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
it "adds policy meta data" do
|
378
|
+
subject.policy(custom_klass, "Developer")
|
379
|
+
expect(subject.meta_data[:foo]).to eq "bar"
|
380
|
+
end
|
381
|
+
|
382
|
+
it "can take an instance not in the registry" do
|
383
|
+
subject.policy(custom_klass.new("Developer"), "ignore this")
|
384
|
+
|
385
|
+
resolve(subject, a_key: "Ismael").tap do |r|
|
386
|
+
expect(r.eligible?).to be true
|
387
|
+
no_errors
|
388
|
+
expect(r.value).to eq "Developer Ismael"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
it 'add policy exceptions to #errors' do
|
393
|
+
register_coercion :error, ->(v, k, c){ raise "This is an error" }
|
394
|
+
|
395
|
+
subject.policy(:error)
|
396
|
+
|
397
|
+
resolve(subject, a_key: "b").tap do |r|
|
398
|
+
expect(r.eligible?).to be true
|
399
|
+
has_error("$", "This is an error")
|
400
|
+
expect(r.value).to eq "b"
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'default coercions' do
|
4
|
+
def test_coercion(key, value, expected)
|
5
|
+
coercion = Parametric.registry.coercions[key]
|
6
|
+
expect(coercion.new.coerce(value, nil, nil)).to eq expected
|
7
|
+
end
|
8
|
+
|
9
|
+
describe ':datetime' do
|
10
|
+
it {
|
11
|
+
coercion = Parametric.registry.coercions[:datetime]
|
12
|
+
coercion.new.coerce("2016-11-05T14:23:34Z", nil, nil).tap do |d|
|
13
|
+
expect(d).to be_a Date
|
14
|
+
expect(d.year).to eq 2016
|
15
|
+
expect(d.month).to eq 11
|
16
|
+
expect(d.day).to eq 5
|
17
|
+
expect(d.hour).to eq 14
|
18
|
+
expect(d.minute).to eq 23
|
19
|
+
expect(d.second).to eq 34
|
20
|
+
expect(d.zone).to eq "+00:00"
|
21
|
+
end
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
describe ':integer' do
|
26
|
+
it {
|
27
|
+
test_coercion(:integer, '10', 10)
|
28
|
+
test_coercion(:integer, '10.20', 10)
|
29
|
+
test_coercion(:integer, 10.20, 10)
|
30
|
+
test_coercion(:integer, 10, 10)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
describe ':number' do
|
35
|
+
it {
|
36
|
+
test_coercion(:number, '10', 10.0)
|
37
|
+
test_coercion(:number, '10.20', 10.20)
|
38
|
+
test_coercion(:number, 10.20, 10.20)
|
39
|
+
test_coercion(:number, 10, 10.0)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
describe ':string' do
|
44
|
+
it {
|
45
|
+
test_coercion(:string, '10', '10')
|
46
|
+
test_coercion(:string, '10.20', '10.20')
|
47
|
+
test_coercion(:string, 10.20, '10.2')
|
48
|
+
test_coercion(:string, 10, '10')
|
49
|
+
test_coercion(:string, true, 'true')
|
50
|
+
test_coercion(:string, 'hello', 'hello')
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
describe ':boolean' do
|
55
|
+
it {
|
56
|
+
test_coercion(:boolean, true, true)
|
57
|
+
test_coercion(:boolean, '10', true)
|
58
|
+
test_coercion(:boolean, '', true)
|
59
|
+
test_coercion(:boolean, nil, false)
|
60
|
+
test_coercion(:boolean, false, false)
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
describe ':split' do
|
65
|
+
it {
|
66
|
+
test_coercion(:split, 'aaa,bb,cc', ['aaa', 'bb', 'cc'])
|
67
|
+
test_coercion(:split, 'aaa ,bb, cc', ['aaa', 'bb', 'cc'])
|
68
|
+
test_coercion(:split, 'aaa', ['aaa'])
|
69
|
+
test_coercion(:split, ['aaa', 'bb', 'cc'], ['aaa', 'bb', 'cc'])
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|