parametric 0.2.10 → 0.2.19
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +4 -0
- data/README.md +265 -36
- data/bench/struct_bench.rb +53 -0
- data/lib/parametric/block_validator.rb +2 -0
- data/lib/parametric/context.rb +6 -1
- data/lib/parametric/default_types.rb +2 -0
- data/lib/parametric/dsl.rb +2 -0
- data/lib/parametric/field.rb +62 -25
- data/lib/parametric/field_dsl.rb +2 -0
- data/lib/parametric/policies.rb +28 -0
- data/lib/parametric/policy_adapter.rb +57 -0
- data/lib/parametric/registry.rb +2 -0
- data/lib/parametric/results.rb +2 -0
- data/lib/parametric/schema.rb +39 -3
- data/lib/parametric/struct.rb +30 -20
- data/lib/parametric/tagged_one_of.rb +134 -0
- data/lib/parametric/version.rb +3 -1
- data/lib/parametric.rb +2 -0
- data/parametric.gemspec +1 -2
- data/spec/field_spec.rb +32 -0
- data/spec/policies_spec.rb +1 -1
- data/spec/schema_lifecycle_hooks_spec.rb +133 -0
- data/spec/schema_spec.rb +81 -0
- data/spec/struct_spec.rb +32 -35
- data/spec/validators_spec.rb +7 -0
- metadata +13 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98676c8edebba19bd1adff020f5eadf9dce07e44f89125be4f0451d6bdee50cf
|
4
|
+
data.tar.gz: 5a5d0ecded864e5185d2b1d1cafe5eeb2307d92cf52c4111e7dd85ebe65c0321
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a3b81c816a02516450bdeb70ddb5b804741481ec0084167f31cdddcef4c3734f25b0d69da022048d9889c7b47a462cd821bf3a4cef07d719aff9b57293e10ef
|
7
|
+
data.tar.gz: 5a2cd8dd4d68cd896901d744f656fca3385f6d3763bc05d62dbd1b6c53362a32092781e7bd57e439b27c6e0071d1179a0355372778d33b9c96f893e6fd4e07d3
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -127,6 +127,60 @@ person_schema = Parametric::Schema.new do |sc, options|
|
|
127
127
|
sc.field(:friends).type(:array).schema(friends_schema)
|
128
128
|
end
|
129
129
|
```
|
130
|
+
|
131
|
+
## Tagged One Of (multiple nested schemas, discriminated by payload key).
|
132
|
+
|
133
|
+
You can use `Field#tagged_one_of` to resolve a nested schema based on the value of a top-level field.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
user_schema = Parametric::Schema.new do |sc, _|
|
137
|
+
field(:name).type(:string).present
|
138
|
+
field(:age).type(:integer).present
|
139
|
+
end
|
140
|
+
|
141
|
+
company_schema = Parametric::Schema.new do
|
142
|
+
field(:name).type(:string).present
|
143
|
+
field(:company_code).type(:string).present
|
144
|
+
end
|
145
|
+
|
146
|
+
schema = Parametric::Schema.new do |sc, _|
|
147
|
+
# Use :type field to locate the sub-schema to use for :sub
|
148
|
+
sc.field(:type).type(:string)
|
149
|
+
|
150
|
+
# Use the :one_of policy to select the sub-schema based on the :type field above
|
151
|
+
sc.field(:sub).type(:object).tagged_one_of do |sub|
|
152
|
+
sub.index_by(:type)
|
153
|
+
sub.on('user', user_schema)
|
154
|
+
sub.on('company', company_schema)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# The schema will now select the correct sub-schema based on the value of :type
|
159
|
+
result = schema.resolve(type: 'user', sub: { name: 'Joe', age: 30 })
|
160
|
+
|
161
|
+
# Instances can also be created separately and used as a policy:
|
162
|
+
|
163
|
+
UserOrCompany = Parametric::TaggedOneOf.new do |sc, _|
|
164
|
+
sc.on('user', user_schema)
|
165
|
+
sc.on('company', company_schema)
|
166
|
+
end
|
167
|
+
|
168
|
+
schema = Parametric::Schema.new do |sc, _|
|
169
|
+
sc.field(:type).type(:string)
|
170
|
+
sc.field(:sub).type(:object).policy(UserOrCompany.index_by(:type))
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
`#index_by` can take a block to decide what value to resolve schemas by:
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
sc.field(:sub).type(:object).tagged_one_of do |sub|
|
178
|
+
sub.index_by { |payload| payload[:entity_type] }
|
179
|
+
sub.on('user', user_schema)
|
180
|
+
sub.on('company', company_schema)
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
130
184
|
## Built-in policies
|
131
185
|
|
132
186
|
Type coercions (the `type` method) and validations (the `validate` method) are all _policies_.
|
@@ -275,62 +329,106 @@ Useful for parsing comma-separated query-string parameters.
|
|
275
329
|
field(:status).policy(:split) # turns "pending,confirmed" into ["pending", "confirmed"]
|
276
330
|
```
|
277
331
|
|
332
|
+
### :value
|
333
|
+
|
334
|
+
A policy to return a static value
|
335
|
+
|
336
|
+
```ruby
|
337
|
+
field(:currency).policy(:value, 'gbp') # this field always resolves to 'gbp'
|
338
|
+
```
|
339
|
+
|
278
340
|
## Custom policies
|
279
341
|
|
280
|
-
You can also register your own custom policy objects.
|
342
|
+
You can also register your own custom policy objects.
|
343
|
+
A policy consist of the following:
|
344
|
+
|
345
|
+
* A `PolicyFactory` interface:
|
281
346
|
|
282
347
|
```ruby
|
283
348
|
class MyPolicy
|
284
|
-
#
|
285
|
-
|
286
|
-
|
349
|
+
# Initializer signature is up to you.
|
350
|
+
# These are the arguments passed to the policy when using in a Field,
|
351
|
+
# ex. field(:name).policy(:my_policy, 'arg1', 'arg2')
|
352
|
+
def initialize(arg1, arg2)
|
353
|
+
@arg1, @arg2 = arg1, arg2
|
287
354
|
end
|
288
355
|
|
289
|
-
#
|
290
|
-
|
291
|
-
|
292
|
-
|
356
|
+
# @return [Hash]
|
357
|
+
def meta_data
|
358
|
+
{ type: :string }
|
359
|
+
end
|
360
|
+
|
361
|
+
# Buld a Policy Runner, which is instantiated
|
362
|
+
# for each field when resolving a schema
|
363
|
+
# @param key [Symbol]
|
364
|
+
# @param value [Any]
|
365
|
+
# @option payload [Hash]
|
366
|
+
# @option context [Parametric::Context]
|
367
|
+
# @return [PolicyRunner]
|
368
|
+
def build(key, value, payload:, context:)
|
369
|
+
MyPolicyRunner.new(key, value, payload, context)
|
370
|
+
end
|
371
|
+
end
|
372
|
+
```
|
373
|
+
|
374
|
+
* A `PolicyRunner` interface.
|
375
|
+
|
376
|
+
```ruby
|
377
|
+
class MyPolicyRunner
|
378
|
+
# Initializer is up to you. See `MyPolicy#build`
|
379
|
+
def initialize(key, value, payload, context)
|
380
|
+
|
293
381
|
end
|
294
382
|
|
295
|
-
#
|
296
|
-
|
297
|
-
|
383
|
+
# Should this policy run at all?
|
384
|
+
# returning [false] halts the field policy chain.
|
385
|
+
# @return [Boolean]
|
386
|
+
def eligible?
|
387
|
+
true
|
298
388
|
end
|
299
389
|
|
300
|
-
#
|
301
|
-
|
390
|
+
# If [false], add [#message] to result errors and halt processing field.
|
391
|
+
# @return [Boolean]
|
392
|
+
def valid?
|
302
393
|
true
|
303
394
|
end
|
304
395
|
|
305
|
-
#
|
306
|
-
|
307
|
-
|
396
|
+
# Coerce the value, or return as-is.
|
397
|
+
# @return [Any]
|
398
|
+
def value
|
399
|
+
@value
|
400
|
+
end
|
401
|
+
|
402
|
+
# Error message for this policy
|
403
|
+
# @return [String]
|
404
|
+
def message
|
405
|
+
"#{@value} is invalid"
|
308
406
|
end
|
309
407
|
end
|
310
408
|
```
|
311
409
|
|
312
|
-
|
410
|
+
Then register your custom policy factory:
|
313
411
|
|
314
412
|
```ruby
|
315
|
-
Parametric.policy :
|
413
|
+
Parametric.policy :my_polict, MyPolicy
|
316
414
|
```
|
317
415
|
|
318
416
|
And then refer to it by name when declaring your schema fields
|
319
417
|
|
320
418
|
```ruby
|
321
|
-
field(:title).policy(:my_policy)
|
419
|
+
field(:title).policy(:my_policy, 'arg1', 'arg2')
|
322
420
|
```
|
323
421
|
|
324
422
|
You can chain custom policies with other policies.
|
325
423
|
|
326
424
|
```ruby
|
327
|
-
field(:title).required.policy(:my_policy)
|
425
|
+
field(:title).required.policy(:my_policy, 'arg1', 'arg2')
|
328
426
|
```
|
329
427
|
|
330
428
|
Note that you can also register instances.
|
331
429
|
|
332
430
|
```ruby
|
333
|
-
Parametric.policy :my_policy, MyPolicy.new
|
431
|
+
Parametric.policy :my_policy, MyPolicy.new('arg1', 'arg2')
|
334
432
|
```
|
335
433
|
|
336
434
|
For example, a policy that can be configured on a field-by-field basis:
|
@@ -341,27 +439,34 @@ class AddJobTitle
|
|
341
439
|
@job_title = job_title
|
342
440
|
end
|
343
441
|
|
344
|
-
def
|
345
|
-
|
442
|
+
def build(key, value, payload:, context:)
|
443
|
+
Runner.new(@job_title, key, value, payload, context)
|
346
444
|
end
|
347
445
|
|
348
|
-
|
349
|
-
|
350
|
-
true
|
446
|
+
def meta_data
|
447
|
+
{}
|
351
448
|
end
|
352
449
|
|
353
|
-
|
354
|
-
|
355
|
-
"#{value}, #{@job_title}"
|
356
|
-
end
|
450
|
+
class Runner
|
451
|
+
attr_reader :message
|
357
452
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
453
|
+
def initialize(job_title, key, value, payload, _context)
|
454
|
+
@job_title = job_title
|
455
|
+
@key, @value, @payload = key, value, payload
|
456
|
+
@message = 'is invalid'
|
457
|
+
end
|
362
458
|
|
363
|
-
|
364
|
-
|
459
|
+
def eligible?
|
460
|
+
true
|
461
|
+
end
|
462
|
+
|
463
|
+
def valid?
|
464
|
+
true
|
465
|
+
end
|
466
|
+
|
467
|
+
def value
|
468
|
+
"#{@value}, #{@job_title}"
|
469
|
+
end
|
365
470
|
end
|
366
471
|
end
|
367
472
|
|
@@ -849,6 +954,130 @@ results.errors["$.Weight"] # => ["is required and value must be present"]
|
|
849
954
|
|
850
955
|
NOTES: dynamically expanded field names are not included in `Schema#structure` metadata, and they are only processes if fields with the given expressions are present in the payload. This means that validations applied to those fields only run if keys are present in the first place.
|
851
956
|
|
957
|
+
## Before and after resolve hooks
|
958
|
+
|
959
|
+
`Schema#before_resolve` can be used to register blocks to modify the entire input payload _before_ individual fields are validated and coerced.
|
960
|
+
This can be useful when you need to pre-populate fields relative to other fields' values, or fetch extra data from other sources.
|
961
|
+
|
962
|
+
```ruby
|
963
|
+
# This example computes the value of the :slug field based on :name
|
964
|
+
schema = Parametric::Schema.new do
|
965
|
+
# Note1: These blocks run before field validations, so :name might be blank or invalid at this point.
|
966
|
+
# Note2: Before hooks _must_ return a payload hash.
|
967
|
+
before_resolve do |payload, context|
|
968
|
+
payload.merge(
|
969
|
+
slug: payload[:name].to_s.downcase.gsub(/\s+/, '-')
|
970
|
+
)
|
971
|
+
end
|
972
|
+
|
973
|
+
# You still need to define the fields you want
|
974
|
+
field(:name).type(:string).present
|
975
|
+
field(:slug).type(:string).present
|
976
|
+
end
|
977
|
+
|
978
|
+
result = schema.resolve( name: 'Joe Bloggs' )
|
979
|
+
result.output # => { name: 'Joe Bloggs', slug: 'joe-bloggs' }
|
980
|
+
```
|
981
|
+
|
982
|
+
Before hooks can be added to nested schemas, too:
|
983
|
+
|
984
|
+
```ruby
|
985
|
+
schema = Parametric::Schema.new do
|
986
|
+
field(:friends).type(:array).schema do
|
987
|
+
before_resolve do |friend_payload, context|
|
988
|
+
friend_payload.merge(title: "Mr/Ms #{friend_payload[:name]}")
|
989
|
+
end
|
990
|
+
|
991
|
+
field(:name).type(:string)
|
992
|
+
field(:title).type(:string)
|
993
|
+
end
|
994
|
+
end
|
995
|
+
```
|
996
|
+
|
997
|
+
You can use inline blocks, but anything that responds to `#call(payload, context)` will work, too:
|
998
|
+
|
999
|
+
```ruby
|
1000
|
+
class SlugMaker
|
1001
|
+
def initialize(slug_field, from:)
|
1002
|
+
@slug_field, @from = slug_field, from
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
def call(payload, context)
|
1006
|
+
payload.merge(
|
1007
|
+
@slug_field => payload[@from].to_s.downcase.gsub(/\s+/, '-')
|
1008
|
+
)
|
1009
|
+
end
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
schema = Parametric::Schema.new do
|
1013
|
+
before_resolve SlugMaker.new(:slug, from: :name)
|
1014
|
+
|
1015
|
+
field(:name).type(:string)
|
1016
|
+
field(:slug).type(:slug)
|
1017
|
+
end
|
1018
|
+
```
|
1019
|
+
|
1020
|
+
The `context` argument can be used to add custom validation errors in a before hook block.
|
1021
|
+
|
1022
|
+
```ruby
|
1023
|
+
schema = Parametric::Schema.new do
|
1024
|
+
before_resolve do |payload, context|
|
1025
|
+
# validate that there's no duplicate friend names
|
1026
|
+
friends = payload[:friends] || []
|
1027
|
+
if friends.any? && friends.map{ |fr| fr[:name] }.uniq.size < friends.size
|
1028
|
+
context.add_error 'friend names must be unique'
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
# don't forget to return the payload
|
1032
|
+
payload
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
field(:friends).type(:array).schema do
|
1036
|
+
field(:name).type(:string)
|
1037
|
+
end
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
result = schema.resolve(
|
1041
|
+
friends: [
|
1042
|
+
{name: 'Joe Bloggs'},
|
1043
|
+
{name: 'Joan Bloggs'},
|
1044
|
+
{name: 'Joe Bloggs'}
|
1045
|
+
]
|
1046
|
+
)
|
1047
|
+
|
1048
|
+
result.valid? # => false
|
1049
|
+
result.errors # => {'$' => ['friend names must be unique']}
|
1050
|
+
```
|
1051
|
+
|
1052
|
+
In most cases you should be validating individual fields using field policies. Only validate in before hooks in cases you have dependencies between fields.
|
1053
|
+
|
1054
|
+
`Schema#after_resolve` takes the sanitized input hash, and can be used to further validate fields that depend on eachother.
|
1055
|
+
|
1056
|
+
```ruby
|
1057
|
+
schema = Parametric::Schema.new do
|
1058
|
+
after_resolve do |payload, ctx|
|
1059
|
+
# Add a top level error using an arbitrary key name
|
1060
|
+
ctx.add_base_error('deposit', 'cannot be greater than house price') if payload[:deposit] > payload[:house_price]
|
1061
|
+
# Or add an error keyed after the current position in the schema
|
1062
|
+
# ctx.add_error('some error') if some_condition
|
1063
|
+
# after_resolve hooks must also return the payload, or a modified copy of it
|
1064
|
+
# note that any changes added here won't be validated.
|
1065
|
+
payload.merge(desc: 'hello')
|
1066
|
+
end
|
1067
|
+
|
1068
|
+
field(:deposit).policy(:integer).present
|
1069
|
+
field(:house_price).policy(:integer).present
|
1070
|
+
field(:desc).policy(:string)
|
1071
|
+
end
|
1072
|
+
|
1073
|
+
result = schema.resolve({ deposit: 1100, house_price: 1000 })
|
1074
|
+
result.valid? # false
|
1075
|
+
result.errors[:deposit] # ['cannot be greater than house price']
|
1076
|
+
result.output[:deposit] # 1100
|
1077
|
+
result.output[:house_price] # 1000
|
1078
|
+
result.output[:desc] # 'hello'
|
1079
|
+
```
|
1080
|
+
|
852
1081
|
## Structs
|
853
1082
|
|
854
1083
|
Structs turn schema definitions into objects graphs with attribute readers.
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'benchmark/ips'
|
2
|
+
require 'parametric/struct'
|
3
|
+
|
4
|
+
StructAccount = Struct.new(:id, :email, keyword_init: true)
|
5
|
+
StructFriend = Struct.new(:name, keyword_init: true)
|
6
|
+
StructUser = Struct.new(:name, :age, :friends, :account, keyword_init: true)
|
7
|
+
|
8
|
+
class ParametricAccount
|
9
|
+
include Parametric::Struct
|
10
|
+
schema do
|
11
|
+
field(:id).type(:integer).present
|
12
|
+
field(:email).type(:string)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ParametricUser
|
17
|
+
include Parametric::Struct
|
18
|
+
schema do
|
19
|
+
field(:name).type(:string).present
|
20
|
+
field(:age).type(:integer).default(42)
|
21
|
+
field(:friends).type(:array).schema do
|
22
|
+
field(:name).type(:string).present
|
23
|
+
end
|
24
|
+
field(:account).type(:object).schema ParametricAccount
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
Benchmark.ips do |x|
|
29
|
+
x.report("Struct") {
|
30
|
+
StructUser.new(
|
31
|
+
name: 'Ismael',
|
32
|
+
age: 42,
|
33
|
+
friends: [
|
34
|
+
StructFriend.new(name: 'Joe'),
|
35
|
+
StructFriend.new(name: 'Joan'),
|
36
|
+
],
|
37
|
+
account: StructAccount.new(id: 123, email: 'my@account.com')
|
38
|
+
)
|
39
|
+
}
|
40
|
+
x.report("Parametric::Struct") {
|
41
|
+
ParametricUser.new!(
|
42
|
+
name: 'Ismael',
|
43
|
+
age: 42,
|
44
|
+
friends: [
|
45
|
+
{ name: 'Joe' },
|
46
|
+
{ name: 'Joan' }
|
47
|
+
],
|
48
|
+
account: { id: 123, email: 'my@account.com' }
|
49
|
+
)
|
50
|
+
}
|
51
|
+
x.compare!
|
52
|
+
end
|
53
|
+
|
data/lib/parametric/context.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Parametric
|
2
4
|
class Top
|
3
5
|
attr_reader :errors
|
@@ -26,6 +28,10 @@ module Parametric
|
|
26
28
|
top.add_error(string_path, msg)
|
27
29
|
end
|
28
30
|
|
31
|
+
def add_base_error(key, msg)
|
32
|
+
top.add_error(key, msg)
|
33
|
+
end
|
34
|
+
|
29
35
|
def sub(key)
|
30
36
|
self.class.new(path + [key], top)
|
31
37
|
end
|
@@ -40,5 +46,4 @@ module Parametric
|
|
40
46
|
end.join
|
41
47
|
end
|
42
48
|
end
|
43
|
-
|
44
49
|
end
|
data/lib/parametric/dsl.rb
CHANGED
data/lib/parametric/field.rb
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
require 'parametric/field_dsl'
|
5
|
+
require 'parametric/policy_adapter'
|
6
|
+
require 'parametric/tagged_one_of'
|
2
7
|
|
3
8
|
module Parametric
|
4
9
|
class ConfigurationError < StandardError; end
|
@@ -37,15 +42,32 @@ module Parametric
|
|
37
42
|
end
|
38
43
|
alias_method :type, :policy
|
39
44
|
|
45
|
+
def tagged_one_of(instance = nil, &block)
|
46
|
+
policy(instance || Parametric::TaggedOneOf.new(&block))
|
47
|
+
end
|
48
|
+
|
40
49
|
def schema(sc = nil, &block)
|
41
50
|
sc = (sc ? sc : Schema.new(&block))
|
42
51
|
meta schema: sc
|
43
52
|
policy sc.schema
|
44
53
|
end
|
45
54
|
|
55
|
+
def from(another_field)
|
56
|
+
meta another_field.meta_data
|
57
|
+
another_field.policies.each do |plc|
|
58
|
+
policies << plc
|
59
|
+
end
|
60
|
+
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def has_policy?(key)
|
65
|
+
policies.any? { |pol| pol.key == key }
|
66
|
+
end
|
67
|
+
|
46
68
|
def visit(meta_key = nil, &visitor)
|
47
69
|
if sc = meta_data[:schema]
|
48
|
-
r = sc.visit(meta_key, &visitor)
|
70
|
+
r = sc.schema.visit(meta_key, &visitor)
|
49
71
|
(meta_data[:type] == :array) ? [r] : r
|
50
72
|
else
|
51
73
|
meta_key ? meta_data[meta_key] : yield(self)
|
@@ -63,37 +85,39 @@ module Parametric
|
|
63
85
|
end
|
64
86
|
|
65
87
|
policies.each do |policy|
|
66
|
-
|
67
|
-
|
68
|
-
if
|
69
|
-
eligible =
|
70
|
-
|
88
|
+
begin
|
89
|
+
pol = policy.build(key, value, payload:, context:)
|
90
|
+
if !pol.eligible?
|
91
|
+
eligible = false
|
92
|
+
if has_default?
|
93
|
+
eligible = true
|
94
|
+
value = default_block.call(key, payload, context)
|
95
|
+
end
|
96
|
+
break
|
97
|
+
else
|
98
|
+
value = pol.value
|
99
|
+
if !pol.valid?
|
100
|
+
eligible = true # eligible, but has errors
|
101
|
+
context.add_error pol.message
|
102
|
+
break # only one error at a time
|
103
|
+
end
|
71
104
|
end
|
105
|
+
rescue StandardError => e
|
106
|
+
context.add_error e.message
|
72
107
|
break
|
73
|
-
else
|
74
|
-
value = resolve_one(policy, value, context)
|
75
|
-
if !policy.valid?(value, key, payload)
|
76
|
-
eligible = true # eligible, but has errors
|
77
|
-
context.add_error policy.message
|
78
|
-
break # only one error at a time
|
79
|
-
end
|
80
108
|
end
|
81
109
|
end
|
82
110
|
|
83
111
|
Result.new(eligible, value)
|
84
112
|
end
|
85
113
|
|
114
|
+
protected
|
115
|
+
|
116
|
+
attr_reader :policies
|
117
|
+
|
86
118
|
private
|
87
|
-
|
88
|
-
|
89
|
-
def resolve_one(policy, value, context)
|
90
|
-
begin
|
91
|
-
policy.coerce(value, key, context)
|
92
|
-
rescue StandardError => e
|
93
|
-
context.add_error e.message
|
94
|
-
value
|
95
|
-
end
|
96
|
-
end
|
119
|
+
|
120
|
+
attr_reader :registry, :default_block
|
97
121
|
|
98
122
|
def has_default?
|
99
123
|
!!default_block && !meta_data[:skip_default]
|
@@ -104,7 +128,20 @@ module Parametric
|
|
104
128
|
|
105
129
|
raise ConfigurationError, "No policies defined for #{key.inspect}" unless obj
|
106
130
|
|
107
|
-
obj
|
131
|
+
obj = obj.new(*args) if obj.respond_to?(:new)
|
132
|
+
obj = PolicyWithKey.new(obj, key)
|
133
|
+
obj = PolicyAdapter.new(obj) unless obj.respond_to?(:build)
|
134
|
+
|
135
|
+
obj
|
136
|
+
end
|
137
|
+
|
138
|
+
class PolicyWithKey < SimpleDelegator
|
139
|
+
attr_reader :key
|
140
|
+
|
141
|
+
def initialize(policy, key)
|
142
|
+
super policy
|
143
|
+
@key = key
|
144
|
+
end
|
108
145
|
end
|
109
146
|
end
|
110
147
|
end
|
data/lib/parametric/field_dsl.rb
CHANGED
data/lib/parametric/policies.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Parametric
|
2
4
|
module Policies
|
3
5
|
class Format
|
@@ -24,6 +26,31 @@ module Parametric
|
|
24
26
|
{}
|
25
27
|
end
|
26
28
|
end
|
29
|
+
|
30
|
+
class Value
|
31
|
+
attr_reader :message
|
32
|
+
|
33
|
+
def initialize(val, msg = 'invalid value')
|
34
|
+
@message = msg
|
35
|
+
@val = val
|
36
|
+
end
|
37
|
+
|
38
|
+
def eligible?(value, key, payload)
|
39
|
+
payload.key?(key)
|
40
|
+
end
|
41
|
+
|
42
|
+
def coerce(_value, _key, _context)
|
43
|
+
@val
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid?(value, key, payload)
|
47
|
+
!payload.key?(key) || !!(value == @val)
|
48
|
+
end
|
49
|
+
|
50
|
+
def meta_data
|
51
|
+
{ value: @val }
|
52
|
+
end
|
53
|
+
end
|
27
54
|
end
|
28
55
|
|
29
56
|
# Default validators
|
@@ -31,6 +58,7 @@ module Parametric
|
|
31
58
|
|
32
59
|
Parametric.policy :format, Policies::Format
|
33
60
|
Parametric.policy :email, Policies::Format.new(EMAIL_REGEXP, 'invalid email')
|
61
|
+
Parametric.policy :value, Policies::Value
|
34
62
|
|
35
63
|
Parametric.policy :noop do
|
36
64
|
eligible do |value, key, payload|
|