parametric 0.2.9 → 0.2.14
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 +124 -0
- data/bench/struct_bench.rb +53 -0
- data/lib/parametric.rb +2 -0
- data/lib/parametric/block_validator.rb +6 -4
- 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 +17 -1
- data/lib/parametric/field_dsl.rb +2 -0
- data/lib/parametric/policies.rb +2 -0
- data/lib/parametric/registry.rb +2 -0
- data/lib/parametric/results.rb +2 -0
- data/lib/parametric/schema.rb +36 -2
- data/lib/parametric/struct.rb +30 -20
- data/lib/parametric/version.rb +3 -1
- data/parametric.gemspec +0 -1
- data/spec/field_spec.rb +14 -0
- data/spec/policies_spec.rb +1 -1
- data/spec/schema_lifecycle_hooks_spec.rb +133 -0
- data/spec/struct_spec.rb +4 -26
- metadata +9 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 40164472fa5f68d604b2f20b6823b4b7c0ca81136c46826705d3a3a4b2e903f2
|
4
|
+
data.tar.gz: 1bce90d98794e1cc1a169bea9ff0b6d606486a9a2e33b95b0e461cfaae567c63
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b93f931277e786ea4f6a39d2db2a8152be14a9887c4a31bfb54a38c5127c85bf43b6e35e442b4b9cc40531879d5638ed234b4e62102cffb23c5b2bf12552c6e
|
7
|
+
data.tar.gz: ca54de8f1cc1aaf4b0cb4805197e634ab07ecabf31cab56a54e3fca330312c3b247a984d5efabef9350729008e1f319730a74b1b068f6685eb6f9c74ddda71aa
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -849,6 +849,130 @@ results.errors["$.Weight"] # => ["is required and value must be present"]
|
|
849
849
|
|
850
850
|
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
851
|
|
852
|
+
## Before and after resolve hooks
|
853
|
+
|
854
|
+
`Schema#before_resolve` can be used to register blocks to modify the entire input payload _before_ individual fields are validated and coerced.
|
855
|
+
This can be useful when you need to pre-populate fields relative to other fields' values, or fetch extra data from other sources.
|
856
|
+
|
857
|
+
```ruby
|
858
|
+
# This example computes the value of the :slug field based on :name
|
859
|
+
schema = Parametric::Schema.new do
|
860
|
+
# Note1: These blocks run before field validations, so :name might be blank or invalid at this point.
|
861
|
+
# Note2: Before hooks _must_ return a payload hash.
|
862
|
+
before_resolve do |payload, context|
|
863
|
+
payload.merge(
|
864
|
+
slug: payload[:name].to_s.downcase.gsub(/\s+/, '-')
|
865
|
+
)
|
866
|
+
end
|
867
|
+
|
868
|
+
# You still need to define the fields you want
|
869
|
+
field(:name).type(:string).present
|
870
|
+
field(:slug).type(:string).present
|
871
|
+
end
|
872
|
+
|
873
|
+
result = schema.resolve( name: 'Joe Bloggs' )
|
874
|
+
result.output # => { name: 'Joe Bloggs', slug: 'joe-bloggs' }
|
875
|
+
```
|
876
|
+
|
877
|
+
Before hooks can be added to nested schemas, too:
|
878
|
+
|
879
|
+
```ruby
|
880
|
+
schema = Parametric::Schema.new do
|
881
|
+
field(:friends).type(:array).schema do
|
882
|
+
before_resolve do |friend_payload, context|
|
883
|
+
friend_payload.merge(title: "Mr/Ms #{friend_payload[:name]}")
|
884
|
+
end
|
885
|
+
|
886
|
+
field(:name).type(:string)
|
887
|
+
field(:title).type(:string)
|
888
|
+
end
|
889
|
+
end
|
890
|
+
```
|
891
|
+
|
892
|
+
You can use inline blocks, but anything that responds to `#call(payload, context)` will work, too:
|
893
|
+
|
894
|
+
```ruby
|
895
|
+
class SlugMaker
|
896
|
+
def initialize(slug_field, from:)
|
897
|
+
@slug_field, @from = slug_field, from
|
898
|
+
end
|
899
|
+
|
900
|
+
def call(payload, context)
|
901
|
+
payload.merge(
|
902
|
+
@slug_field => payload[@from].to_s.downcase.gsub(/\s+/, '-')
|
903
|
+
)
|
904
|
+
end
|
905
|
+
end
|
906
|
+
|
907
|
+
schema = Parametric::Schema.new do
|
908
|
+
before_resolve SlugMaker.new(:slug, from: :name)
|
909
|
+
|
910
|
+
field(:name).type(:string)
|
911
|
+
field(:slug).type(:slug)
|
912
|
+
end
|
913
|
+
```
|
914
|
+
|
915
|
+
The `context` argument can be used to add custom validation errors in a before hook block.
|
916
|
+
|
917
|
+
```ruby
|
918
|
+
schema = Parametric::Schema.new do
|
919
|
+
before_resolve do |payload, context|
|
920
|
+
# validate that there's no duplicate friend names
|
921
|
+
friends = payload[:friends] || []
|
922
|
+
if friends.any? && friends.map{ |fr| fr[:name] }.uniq.size < friends.size
|
923
|
+
context.add_error 'friend names must be unique'
|
924
|
+
end
|
925
|
+
|
926
|
+
# don't forget to return the payload
|
927
|
+
payload
|
928
|
+
end
|
929
|
+
|
930
|
+
field(:friends).type(:array).schema do
|
931
|
+
field(:name).type(:string)
|
932
|
+
end
|
933
|
+
end
|
934
|
+
|
935
|
+
result = schema.resolve(
|
936
|
+
friends: [
|
937
|
+
{name: 'Joe Bloggs'},
|
938
|
+
{name: 'Joan Bloggs'},
|
939
|
+
{name: 'Joe Bloggs'}
|
940
|
+
]
|
941
|
+
)
|
942
|
+
|
943
|
+
result.valid? # => false
|
944
|
+
result.errors # => {'$' => ['friend names must be unique']}
|
945
|
+
```
|
946
|
+
|
947
|
+
In most cases you should be validating individual fields using field policies. Only validate in before hooks in cases you have dependencies between fields.
|
948
|
+
|
949
|
+
`Schema#after_resolve` takes the sanitized input hash, and can be used to further validate fields that depend on eachother.
|
950
|
+
|
951
|
+
```ruby
|
952
|
+
schema = Parametric::Schema.new do
|
953
|
+
after_resolve do |payload, ctx|
|
954
|
+
# Add a top level error using an arbitrary key name
|
955
|
+
ctx.add_base_error('deposit', 'cannot be greater than house price') if payload[:deposit] > payload[:house_price]
|
956
|
+
# Or add an error keyed after the current position in the schema
|
957
|
+
# ctx.add_error('some error') if some_condition
|
958
|
+
# after_resolve hooks must also return the payload, or a modified copy of it
|
959
|
+
# note that any changes added here won't be validated.
|
960
|
+
payload.merge(desc: 'hello')
|
961
|
+
end
|
962
|
+
|
963
|
+
field(:deposit).policy(:integer).present
|
964
|
+
field(:house_price).policy(:integer).present
|
965
|
+
field(:desc).policy(:string)
|
966
|
+
end
|
967
|
+
|
968
|
+
result = schema.resolve({ deposit: 1100, house_price: 1000 })
|
969
|
+
result.valid? # false
|
970
|
+
result.errors[:deposit] # ['cannot be greater than house price']
|
971
|
+
result.output[:deposit] # 1100
|
972
|
+
result.output[:house_price] # 1000
|
973
|
+
result.output[:desc] # 'hello'
|
974
|
+
```
|
975
|
+
|
852
976
|
## Structs
|
853
977
|
|
854
978
|
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.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Parametric
|
2
4
|
class BlockValidator
|
3
5
|
def self.build(meth, &block)
|
@@ -8,12 +10,12 @@ module Parametric
|
|
8
10
|
|
9
11
|
def self.message(&block)
|
10
12
|
@message_block = block if block_given?
|
11
|
-
@message_block
|
13
|
+
@message_block if instance_variable_defined?('@message_block')
|
12
14
|
end
|
13
15
|
|
14
16
|
def self.validate(&validate_block)
|
15
17
|
@validate_block = validate_block if block_given?
|
16
|
-
@validate_block
|
18
|
+
@validate_block if instance_variable_defined?('@validate_block')
|
17
19
|
end
|
18
20
|
|
19
21
|
def self.coerce(&coerce_block)
|
@@ -23,12 +25,12 @@ module Parametric
|
|
23
25
|
|
24
26
|
def self.eligible(&block)
|
25
27
|
@eligible_block = block if block_given?
|
26
|
-
@eligible_block
|
28
|
+
@eligible_block if instance_variable_defined?('@eligible_block')
|
27
29
|
end
|
28
30
|
|
29
31
|
def self.meta_data(&block)
|
30
32
|
@meta_data_block = block if block_given?
|
31
|
-
@meta_data_block
|
33
|
+
@meta_data_block if instance_variable_defined?('@meta_data_block')
|
32
34
|
end
|
33
35
|
|
34
36
|
attr_reader :message
|
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,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "parametric/field_dsl"
|
2
4
|
|
3
5
|
module Parametric
|
@@ -43,6 +45,15 @@ module Parametric
|
|
43
45
|
policy sc.schema
|
44
46
|
end
|
45
47
|
|
48
|
+
def from(another_field)
|
49
|
+
meta another_field.meta_data
|
50
|
+
another_field.policies.each do |plc|
|
51
|
+
policies << plc
|
52
|
+
end
|
53
|
+
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
46
57
|
def visit(meta_key = nil, &visitor)
|
47
58
|
if sc = meta_data[:schema]
|
48
59
|
r = sc.visit(meta_key, &visitor)
|
@@ -83,8 +94,13 @@ module Parametric
|
|
83
94
|
Result.new(eligible, value)
|
84
95
|
end
|
85
96
|
|
97
|
+
protected
|
98
|
+
|
99
|
+
attr_reader :policies
|
100
|
+
|
86
101
|
private
|
87
|
-
|
102
|
+
|
103
|
+
attr_reader :registry, :default_block
|
88
104
|
|
89
105
|
def resolve_one(policy, value, context)
|
90
106
|
begin
|
data/lib/parametric/field_dsl.rb
CHANGED
data/lib/parametric/policies.rb
CHANGED
data/lib/parametric/registry.rb
CHANGED
data/lib/parametric/results.rb
CHANGED
data/lib/parametric/schema.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "parametric/context"
|
2
4
|
require "parametric/results"
|
3
5
|
require "parametric/field"
|
@@ -12,6 +14,8 @@ module Parametric
|
|
12
14
|
@default_field_policies = []
|
13
15
|
@ignored_field_keys = []
|
14
16
|
@expansions = {}
|
17
|
+
@before_hooks = []
|
18
|
+
@after_hooks = []
|
15
19
|
end
|
16
20
|
|
17
21
|
def schema
|
@@ -90,6 +94,20 @@ module Parametric
|
|
90
94
|
end
|
91
95
|
end
|
92
96
|
|
97
|
+
def before_resolve(klass = nil, &block)
|
98
|
+
raise ArgumentError, '#before_resolve expects a callable object, or a block' if !klass && !block_given?
|
99
|
+
callable = klass || block
|
100
|
+
before_hooks << callable
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
def after_resolve(klass = nil, &block)
|
105
|
+
raise ArgumentError, '#after_resolve expects a callable object, or a block' if !klass && !block_given?
|
106
|
+
callable = klass || block
|
107
|
+
after_hooks << callable
|
108
|
+
self
|
109
|
+
end
|
110
|
+
|
93
111
|
def expand(exp, &block)
|
94
112
|
expansions[exp] = block
|
95
113
|
self
|
@@ -139,19 +157,35 @@ module Parametric
|
|
139
157
|
|
140
158
|
protected
|
141
159
|
|
142
|
-
attr_reader :definitions, :options
|
160
|
+
attr_reader :definitions, :options, :before_hooks, :after_hooks
|
143
161
|
|
144
162
|
private
|
145
163
|
|
146
164
|
attr_reader :default_field_policies, :ignored_field_keys, :expansions
|
147
165
|
|
148
166
|
def coerce_one(val, context, flds: fields)
|
149
|
-
|
167
|
+
val = run_before_hooks(val, context)
|
168
|
+
|
169
|
+
out = flds.each_with_object({}) do |(_, field), m|
|
150
170
|
r = field.resolve(val, context.sub(field.key))
|
151
171
|
if r.eligible?
|
152
172
|
m[field.key] = r.value
|
153
173
|
end
|
154
174
|
end
|
175
|
+
|
176
|
+
run_after_hooks(out, context)
|
177
|
+
end
|
178
|
+
|
179
|
+
def run_before_hooks(val, context)
|
180
|
+
before_hooks.reduce(val) do |value, callable|
|
181
|
+
callable.call(value, context)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def run_after_hooks(val, context)
|
186
|
+
after_hooks.reduce(val) do |value, callable|
|
187
|
+
callable.call(value, context)
|
188
|
+
end
|
155
189
|
end
|
156
190
|
|
157
191
|
class MatchContext
|
data/lib/parametric/struct.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'parametric/dsl'
|
2
4
|
|
3
5
|
module Parametric
|
@@ -36,6 +38,10 @@ module Parametric
|
|
36
38
|
_results.output.clone
|
37
39
|
end
|
38
40
|
|
41
|
+
def [](key)
|
42
|
+
_results.output[key.to_sym]
|
43
|
+
end
|
44
|
+
|
39
45
|
def ==(other)
|
40
46
|
other.respond_to?(:to_h) && other.to_h.eql?(to_h)
|
41
47
|
end
|
@@ -56,10 +62,24 @@ module Parametric
|
|
56
62
|
|
57
63
|
# this hook is called after schema definition in DSL module
|
58
64
|
def parametric_after_define_schema(schema)
|
59
|
-
schema.fields.
|
60
|
-
|
61
|
-
|
65
|
+
schema.fields.values.each do |field|
|
66
|
+
if field.meta_data[:schema]
|
67
|
+
if field.meta_data[:schema].is_a?(Parametric::Schema)
|
68
|
+
klass = Class.new do
|
69
|
+
include Struct
|
70
|
+
end
|
71
|
+
klass.schema = field.meta_data[:schema]
|
72
|
+
self.const_set(__class_name(field.key), klass)
|
73
|
+
klass.parametric_after_define_schema(field.meta_data[:schema])
|
74
|
+
else
|
75
|
+
self.const_set(__class_name(field.key), field.meta_data[:schema])
|
76
|
+
end
|
62
77
|
end
|
78
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
79
|
+
def #{field.key}
|
80
|
+
_graph[:#{field.key}]
|
81
|
+
end
|
82
|
+
RUBY
|
63
83
|
end
|
64
84
|
end
|
65
85
|
|
@@ -69,27 +89,11 @@ module Parametric
|
|
69
89
|
end
|
70
90
|
end
|
71
91
|
|
72
|
-
def parametric_build_class_for_child(key, child_schema)
|
73
|
-
klass = Class.new do
|
74
|
-
include Struct
|
75
|
-
end
|
76
|
-
klass.schema = child_schema
|
77
|
-
klass
|
78
|
-
end
|
79
|
-
|
80
92
|
def wrap(key, value)
|
81
|
-
field = schema.fields[key]
|
82
|
-
return value unless field
|
83
|
-
|
84
93
|
case value
|
85
94
|
when Hash
|
86
95
|
# find constructor for field
|
87
|
-
cons =
|
88
|
-
if cons.kind_of?(Parametric::Schema)
|
89
|
-
klass = parametric_build_class_for_child(key, cons)
|
90
|
-
klass.parametric_after_define_schema(cons)
|
91
|
-
cons = klass
|
92
|
-
end
|
96
|
+
cons = self.const_get(__class_name(key))
|
93
97
|
cons ? cons.new(value) : value.freeze
|
94
98
|
when Array
|
95
99
|
value.map{|v| wrap(key, v) }.freeze
|
@@ -97,6 +101,12 @@ module Parametric
|
|
97
101
|
value.freeze
|
98
102
|
end
|
99
103
|
end
|
104
|
+
|
105
|
+
PLURAL_END = /s$/.freeze
|
106
|
+
|
107
|
+
def __class_name(key)
|
108
|
+
key.to_s.split('_').map(&:capitalize).join.sub(PLURAL_END, '')
|
109
|
+
end
|
100
110
|
end
|
101
111
|
end
|
102
112
|
end
|
data/lib/parametric/version.rb
CHANGED
data/parametric.gemspec
CHANGED
@@ -17,7 +17,6 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
-
spec.add_development_dependency "bundler", "~> 1.5"
|
21
20
|
spec.add_development_dependency "rake"
|
22
21
|
spec.add_development_dependency "rspec", '3.4.0'
|
23
22
|
spec.add_development_dependency "byebug"
|
data/spec/field_spec.rb
CHANGED
@@ -112,6 +112,20 @@ describe Parametric::Field do
|
|
112
112
|
end
|
113
113
|
end
|
114
114
|
|
115
|
+
describe '#from' do
|
116
|
+
it 'copies policies and metadata from an existing field' do
|
117
|
+
subject.policy(:string).present.options(['a', 'b', 'c'])
|
118
|
+
|
119
|
+
field = described_class.new(:another_key, registry).from(subject)
|
120
|
+
resolve(field, another_key: "abc").tap do |r|
|
121
|
+
has_errors
|
122
|
+
end
|
123
|
+
expect(field.meta_data[:type]).to eq(:string)
|
124
|
+
expect(field.meta_data[:present]).to be(true)
|
125
|
+
expect(field.meta_data[:options]).to eq(['a', 'b', 'c'])
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
115
129
|
describe "#present" do
|
116
130
|
it "is valid if value is present" do
|
117
131
|
resolve(subject.present, a_key: "abc").tap do |r|
|
data/spec/policies_spec.rb
CHANGED
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Parametric::Schema do
|
4
|
+
describe '#before_resolve' do
|
5
|
+
it 'passes payload through before_resolve block, if defined' do
|
6
|
+
schema = described_class.new do
|
7
|
+
before_resolve do |payload, _context|
|
8
|
+
payload[:slug] = payload[:name].to_s.downcase.gsub(/\s+/, '-') unless payload[:slug]
|
9
|
+
payload
|
10
|
+
end
|
11
|
+
|
12
|
+
field(:name).policy(:string).present
|
13
|
+
field(:slug).policy(:string).present
|
14
|
+
field(:variants).policy(:array).schema do
|
15
|
+
before_resolve do |payload, _context|
|
16
|
+
payload[:slug] = "v: #{payload[:name].to_s.downcase}"
|
17
|
+
payload
|
18
|
+
end
|
19
|
+
field(:name).policy(:string).present
|
20
|
+
field(:slug).type(:string).present
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
result = schema.resolve({ name: 'A name', variants: [{ name: 'A variant' }] })
|
25
|
+
expect(result.valid?).to be true
|
26
|
+
expect(result.output[:slug]).to eq 'a-name'
|
27
|
+
expect(result.output[:variants].first[:slug]).to eq 'v: a variant'
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'collects errors added in before_resolve blocks' do
|
31
|
+
schema = described_class.new do
|
32
|
+
field(:variants).type(:array).schema do
|
33
|
+
before_resolve do |payload, context|
|
34
|
+
context.add_error 'nope!' if payload[:name] == 'with errors'
|
35
|
+
payload
|
36
|
+
end
|
37
|
+
field(:name).type(:string)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
results = schema.resolve({ variants: [ {name: 'no errors'}, {name: 'with errors'}]})
|
42
|
+
expect(results.valid?).to be false
|
43
|
+
expect(results.errors['$.variants[1]']).to eq ['nope!']
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'copies before_resolve hooks to merged schemas' do
|
47
|
+
schema1 = described_class.new do
|
48
|
+
before_resolve do |payload, _context|
|
49
|
+
payload[:slug] = payload[:name].to_s.downcase.gsub(/\s+/, '-') unless payload[:slug]
|
50
|
+
payload
|
51
|
+
end
|
52
|
+
field(:name).present.type(:string)
|
53
|
+
field(:slug).present.type(:string)
|
54
|
+
end
|
55
|
+
|
56
|
+
schema2 = described_class.new do
|
57
|
+
before_resolve do |payload, _context|
|
58
|
+
payload[:slug] = "slug-#{payload[:slug]}" if payload[:slug]
|
59
|
+
payload
|
60
|
+
end
|
61
|
+
|
62
|
+
field(:age).type(:integer)
|
63
|
+
end
|
64
|
+
|
65
|
+
schema3 = schema1.merge(schema2)
|
66
|
+
|
67
|
+
results = schema3.resolve({ name: 'Ismael Celis', age: 41 })
|
68
|
+
expect(results.output[:slug]).to eq 'slug-ismael-celis'
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'works with any callable' do
|
72
|
+
slug_maker = Class.new do
|
73
|
+
def initialize(slug_field, from:)
|
74
|
+
@slug_field, @from = slug_field, from
|
75
|
+
end
|
76
|
+
|
77
|
+
def call(payload, _context)
|
78
|
+
payload.merge(
|
79
|
+
@slug_field => payload[@from].to_s.downcase.gsub(/\s+/, '-')
|
80
|
+
)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
schema = described_class.new do |sc, _opts|
|
85
|
+
sc.before_resolve slug_maker.new(:slug, from: :name)
|
86
|
+
|
87
|
+
sc.field(:name).type(:string)
|
88
|
+
sc.field(:slug).type(:string)
|
89
|
+
end
|
90
|
+
|
91
|
+
results = schema.resolve(name: 'Ismael Celis')
|
92
|
+
expect(results.output[:slug]).to eq 'ismael-celis'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#after_resolve' do
|
97
|
+
let!(:schema) do
|
98
|
+
described_class.new do
|
99
|
+
after_resolve do |payload, ctx|
|
100
|
+
ctx.add_base_error('deposit', 'cannot be greater than house price') if payload[:deposit] > payload[:house_price]
|
101
|
+
payload.merge(desc: 'hello')
|
102
|
+
end
|
103
|
+
|
104
|
+
field(:deposit).policy(:integer).present
|
105
|
+
field(:house_price).policy(:integer).present
|
106
|
+
field(:desc).policy(:string)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'passes payload through after_resolve block, if defined' do
|
111
|
+
result = schema.resolve({ deposit: 1100, house_price: 1000 })
|
112
|
+
expect(result.valid?).to be false
|
113
|
+
expect(result.output[:deposit]).to eq 1100
|
114
|
+
expect(result.output[:house_price]).to eq 1000
|
115
|
+
expect(result.output[:desc]).to eq 'hello'
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'copies after hooks when merging schemas' do
|
119
|
+
child_schema = described_class.new do
|
120
|
+
field(:name).type(:string)
|
121
|
+
end
|
122
|
+
|
123
|
+
union = schema.merge(child_schema)
|
124
|
+
|
125
|
+
result = union.resolve({ name: 'Joe', deposit: 1100, house_price: 1000 })
|
126
|
+
expect(result.valid?).to be false
|
127
|
+
expect(result.output[:deposit]).to eq 1100
|
128
|
+
expect(result.output[:house_price]).to eq 1000
|
129
|
+
expect(result.output[:desc]).to eq 'hello'
|
130
|
+
expect(result.output[:name]).to eq 'Joe'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/spec/struct_spec.rb
CHANGED
@@ -40,6 +40,10 @@ describe Parametric::Struct do
|
|
40
40
|
expect(instance.friends.first.name).to eq 'Ismael'
|
41
41
|
expect(instance.friends.first).to be_a friend_class
|
42
42
|
|
43
|
+
# Hash access with #[]
|
44
|
+
expect(instance[:title]).to eq instance.title
|
45
|
+
expect(instance[:friends].first[:name]).to eq instance.friends.first.name
|
46
|
+
|
43
47
|
invalid_instance = klass.new({
|
44
48
|
friends: [
|
45
49
|
{name: 'Ismael', age: 40},
|
@@ -99,32 +103,6 @@ describe Parametric::Struct do
|
|
99
103
|
expect(instance.friends.first.age).to eq 10
|
100
104
|
end
|
101
105
|
|
102
|
-
it 'wraps nested schemas in custom class' do
|
103
|
-
klass = Class.new do
|
104
|
-
include Parametric::Struct
|
105
|
-
|
106
|
-
def self.parametric_build_class_for_child(key, child_schema)
|
107
|
-
Class.new do
|
108
|
-
include Parametric::Struct
|
109
|
-
schema child_schema
|
110
|
-
def salutation
|
111
|
-
"my age is #{age}"
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
schema do
|
117
|
-
field(:name).type(:string).present
|
118
|
-
field(:friends).type(:array).schema do
|
119
|
-
field(:age).type(:integer)
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
user = klass.new(name: 'Ismael', friends: [{age: 43}])
|
125
|
-
expect(user.friends.first.salutation).to eq 'my age is 43'
|
126
|
-
end
|
127
|
-
|
128
106
|
it "wraps regular schemas in structs" do
|
129
107
|
friend_schema = Parametric::Schema.new do
|
130
108
|
field(:name)
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: parametric
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismael Celis
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: bundler
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '1.5'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '1.5'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: rake
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,6 +67,7 @@ files:
|
|
81
67
|
- LICENSE.txt
|
82
68
|
- README.md
|
83
69
|
- Rakefile
|
70
|
+
- bench/struct_bench.rb
|
84
71
|
- bin/console
|
85
72
|
- lib/parametric.rb
|
86
73
|
- lib/parametric/block_validator.rb
|
@@ -101,6 +88,7 @@ files:
|
|
101
88
|
- spec/expand_spec.rb
|
102
89
|
- spec/field_spec.rb
|
103
90
|
- spec/policies_spec.rb
|
91
|
+
- spec/schema_lifecycle_hooks_spec.rb
|
104
92
|
- spec/schema_spec.rb
|
105
93
|
- spec/schema_walk_spec.rb
|
106
94
|
- spec/spec_helper.rb
|
@@ -110,7 +98,7 @@ homepage: ''
|
|
110
98
|
licenses:
|
111
99
|
- MIT
|
112
100
|
metadata: {}
|
113
|
-
post_install_message:
|
101
|
+
post_install_message:
|
114
102
|
rdoc_options: []
|
115
103
|
require_paths:
|
116
104
|
- lib
|
@@ -125,9 +113,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
113
|
- !ruby/object:Gem::Version
|
126
114
|
version: '0'
|
127
115
|
requirements: []
|
128
|
-
|
129
|
-
|
130
|
-
signing_key:
|
116
|
+
rubygems_version: 3.0.3
|
117
|
+
signing_key:
|
131
118
|
specification_version: 4
|
132
119
|
summary: DSL for declaring allowed parameters with options, regexp patern and default
|
133
120
|
values.
|
@@ -137,6 +124,7 @@ test_files:
|
|
137
124
|
- spec/expand_spec.rb
|
138
125
|
- spec/field_spec.rb
|
139
126
|
- spec/policies_spec.rb
|
127
|
+
- spec/schema_lifecycle_hooks_spec.rb
|
140
128
|
- spec/schema_spec.rb
|
141
129
|
- spec/schema_walk_spec.rb
|
142
130
|
- spec/spec_helper.rb
|