parametric 0.2.7 → 0.2.12
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 +140 -2
- 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 +2 -0
- 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 +50 -7
- data/lib/parametric/struct.rb +26 -20
- data/lib/parametric/version.rb +3 -1
- data/parametric.gemspec +0 -1
- data/spec/policies_spec.rb +1 -1
- data/spec/schema_lifecycle_hooks_spec.rb +133 -0
- data/spec/schema_spec.rb +25 -0
- data/spec/struct_spec.rb +0 -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: c773dd859f56f8d526384567daf66c1c259a7e5d859c9fe40ecc620c0ee73253
|
4
|
+
data.tar.gz: 9b056fd197e9fe84a2b14b16134d860ae3a7c49313f370fd9de82fb918c7bc3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d068ab0c811e30db11d3b6eea2e38937557d2abb36df07623f419b669e7186a86af8de645575cacdb5efe7a51c0dbbe40048aa3fe5417296ee3c5810fd477bf
|
7
|
+
data.tar.gz: da9c1a990c6a0c2b85cf27d3f8ea7dd78e9b2e031548e240a79a43b85de9538e496ed491f3a5ef023ae6939d0f706b74274dd0519a7ae48d6452db65eed0d3ce
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -98,7 +98,7 @@ results.errors # => {"$.friends[0].name" => "is required"}
|
|
98
98
|
You can optionally use an existing schema instance as a nested schema:
|
99
99
|
|
100
100
|
```ruby
|
101
|
-
|
101
|
+
FRIENDS_SCHEMA = Parametric::Schema.new do
|
102
102
|
field(:friends).type(:array).schema do
|
103
103
|
field(:name).type(:string).required
|
104
104
|
field(:email).policy(:email)
|
@@ -109,10 +109,24 @@ person_schema = Parametric::Schema.new do
|
|
109
109
|
field(:name).type(:string).required
|
110
110
|
field(:age).type(:integer)
|
111
111
|
# Nest friends_schema
|
112
|
-
field(:friends).type(:array).schema(
|
112
|
+
field(:friends).type(:array).schema(FRIENDS_SCHEMA)
|
113
113
|
end
|
114
114
|
```
|
115
115
|
|
116
|
+
Note that _person_schema_'s definition has access to `FRIENDS_SCHEMA` because it's a constant.
|
117
|
+
Definition blocks are run in the context of the defining schema instance by default.
|
118
|
+
|
119
|
+
To preserve the original block's context, declare two arguments in your block, the defining schema `sc` and options has.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
person_schema = Parametric::Schema.new do |sc, options|
|
123
|
+
# this block now preserves its context. Call `sc.field` to add fields to the current schema.
|
124
|
+
sc.field(:name).type(:string).required
|
125
|
+
sc.field(:age).type(:integer)
|
126
|
+
# We now have access to local variables
|
127
|
+
sc.field(:friends).type(:array).schema(friends_schema)
|
128
|
+
end
|
129
|
+
```
|
116
130
|
## Built-in policies
|
117
131
|
|
118
132
|
Type coercions (the `type` method) and validations (the `validate` method) are all _policies_.
|
@@ -835,6 +849,130 @@ results.errors["$.Weight"] # => ["is required and value must be present"]
|
|
835
849
|
|
836
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.
|
837
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
|
+
|
838
976
|
## Structs
|
839
977
|
|
840
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
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
|
@@ -44,11 +48,16 @@ module Parametric
|
|
44
48
|
copy_into instance
|
45
49
|
end
|
46
50
|
|
47
|
-
def merge(other_schema)
|
48
|
-
instance
|
51
|
+
def merge(other_schema = nil, &block)
|
52
|
+
raise ArgumentError, '#merge takes either a schema instance or a block' if other_schema.nil? && !block_given?
|
49
53
|
|
50
|
-
|
51
|
-
|
54
|
+
if other_schema
|
55
|
+
instance = self.class.new(options.merge(other_schema.options))
|
56
|
+
copy_into(instance)
|
57
|
+
other_schema.copy_into(instance)
|
58
|
+
else
|
59
|
+
merge(self.class.new(&block))
|
60
|
+
end
|
52
61
|
end
|
53
62
|
|
54
63
|
def copy_into(instance)
|
@@ -85,6 +94,20 @@ module Parametric
|
|
85
94
|
end
|
86
95
|
end
|
87
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
|
+
|
88
111
|
def expand(exp, &block)
|
89
112
|
expansions[exp] = block
|
90
113
|
self
|
@@ -134,19 +157,35 @@ module Parametric
|
|
134
157
|
|
135
158
|
protected
|
136
159
|
|
137
|
-
attr_reader :definitions, :options
|
160
|
+
attr_reader :definitions, :options, :before_hooks, :after_hooks
|
138
161
|
|
139
162
|
private
|
140
163
|
|
141
164
|
attr_reader :default_field_policies, :ignored_field_keys, :expansions
|
142
165
|
|
143
166
|
def coerce_one(val, context, flds: fields)
|
144
|
-
|
167
|
+
val = run_before_hooks(val, context)
|
168
|
+
|
169
|
+
out = flds.each_with_object({}) do |(_, field), m|
|
145
170
|
r = field.resolve(val, context.sub(field.key))
|
146
171
|
if r.eligible?
|
147
172
|
m[field.key] = r.value
|
148
173
|
end
|
149
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
|
150
189
|
end
|
151
190
|
|
152
191
|
class MatchContext
|
@@ -177,7 +216,11 @@ module Parametric
|
|
177
216
|
def apply!
|
178
217
|
return if @applied
|
179
218
|
definitions.each do |d|
|
180
|
-
|
219
|
+
if d.arity == 2 # pass schema instance and options, preserve block context
|
220
|
+
d.call(self, options)
|
221
|
+
else # run block in context of current instance
|
222
|
+
self.instance_exec(options, &d)
|
223
|
+
end
|
181
224
|
end
|
182
225
|
@applied = true
|
183
226
|
end
|
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
|
@@ -56,10 +58,24 @@ module Parametric
|
|
56
58
|
|
57
59
|
# this hook is called after schema definition in DSL module
|
58
60
|
def parametric_after_define_schema(schema)
|
59
|
-
schema.fields.
|
60
|
-
|
61
|
-
|
61
|
+
schema.fields.values.each do |field|
|
62
|
+
if field.meta_data[:schema]
|
63
|
+
if field.meta_data[:schema].is_a?(Parametric::Schema)
|
64
|
+
klass = Class.new do
|
65
|
+
include Struct
|
66
|
+
end
|
67
|
+
klass.schema = field.meta_data[:schema]
|
68
|
+
self.const_set(__class_name(field.key), klass)
|
69
|
+
klass.parametric_after_define_schema(field.meta_data[:schema])
|
70
|
+
else
|
71
|
+
self.const_set(__class_name(field.key), field.meta_data[:schema])
|
72
|
+
end
|
62
73
|
end
|
74
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
75
|
+
def #{field.key}
|
76
|
+
_graph[:#{field.key}]
|
77
|
+
end
|
78
|
+
RUBY
|
63
79
|
end
|
64
80
|
end
|
65
81
|
|
@@ -69,27 +85,11 @@ module Parametric
|
|
69
85
|
end
|
70
86
|
end
|
71
87
|
|
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
88
|
def wrap(key, value)
|
81
|
-
field = schema.fields[key]
|
82
|
-
return value unless field
|
83
|
-
|
84
89
|
case value
|
85
90
|
when Hash
|
86
91
|
# 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
|
92
|
+
cons = self.const_get(__class_name(key))
|
93
93
|
cons ? cons.new(value) : value.freeze
|
94
94
|
when Array
|
95
95
|
value.map{|v| wrap(key, v) }.freeze
|
@@ -97,6 +97,12 @@ module Parametric
|
|
97
97
|
value.freeze
|
98
98
|
end
|
99
99
|
end
|
100
|
+
|
101
|
+
PLURAL_END = /s$/.freeze
|
102
|
+
|
103
|
+
def __class_name(key)
|
104
|
+
key.to_s.split('_').map(&:capitalize).join.sub(PLURAL_END, '')
|
105
|
+
end
|
100
106
|
end
|
101
107
|
end
|
102
108
|
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/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/schema_spec.rb
CHANGED
@@ -169,6 +169,17 @@ describe Parametric::Schema do
|
|
169
169
|
expect(new_schema.fields[:title].meta_data[:type]).to eq :string
|
170
170
|
expect(new_schema.fields[:price].meta_data[:type]).to eq :string
|
171
171
|
end
|
172
|
+
|
173
|
+
it 'can merge from a block' do
|
174
|
+
new_schema = schema1.merge do
|
175
|
+
field(:price).policy(:string)
|
176
|
+
field(:description).policy(:string)
|
177
|
+
end
|
178
|
+
|
179
|
+
expect(schema1.fields[:price].meta_data[:type]).to eq :integer
|
180
|
+
expect(new_schema.fields[:title].meta_data[:type]).to eq :string
|
181
|
+
expect(new_schema.fields[:price].meta_data[:type]).to eq :string
|
182
|
+
end
|
172
183
|
end
|
173
184
|
|
174
185
|
context "with options" do
|
@@ -226,6 +237,20 @@ describe Parametric::Schema do
|
|
226
237
|
end
|
227
238
|
end
|
228
239
|
|
240
|
+
context 'yielding schema to definition, to preserve outer context' do
|
241
|
+
it 'yields schema instance and options to definition block, can access outer context' do
|
242
|
+
schema1 = described_class.new do
|
243
|
+
field(:name).type(:string)
|
244
|
+
end
|
245
|
+
schema2 = described_class.new do |sc, _opts|
|
246
|
+
sc.field(:user).schema schema1
|
247
|
+
end
|
248
|
+
|
249
|
+
out = schema2.resolve(user: { name: 'Joe' }).output
|
250
|
+
expect(out[:user][:name]).to eq 'Joe'
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
229
254
|
describe "#ignore" do
|
230
255
|
it "ignores fields" do
|
231
256
|
s1 = described_class.new.ignore(:title, :status) do
|
data/spec/struct_spec.rb
CHANGED
@@ -99,32 +99,6 @@ describe Parametric::Struct do
|
|
99
99
|
expect(instance.friends.first.age).to eq 10
|
100
100
|
end
|
101
101
|
|
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
102
|
it "wraps regular schemas in structs" do
|
129
103
|
friend_schema = Parametric::Schema.new do
|
130
104
|
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.12
|
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: 2020-08-24 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
|