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
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parametric
|
4
|
+
# Adapt legacy policies to the new policy interface
|
5
|
+
class PolicyAdapter
|
6
|
+
class PolicyRunner
|
7
|
+
def initialize(policy, key, value, payload, context)
|
8
|
+
@policy, @key, @raw_value, @payload, @context = policy, key, value, payload, context
|
9
|
+
end
|
10
|
+
|
11
|
+
# The PolicyRunner interface
|
12
|
+
# @return [Boolean]
|
13
|
+
def eligible?
|
14
|
+
@policy.eligible?(@raw_value, @key, @payload)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Boolean]
|
18
|
+
def valid?
|
19
|
+
@policy.valid?(value, @key, @payload)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Any]
|
23
|
+
def value
|
24
|
+
@value ||= @policy.coerce(@raw_value, @key, @context)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [String]
|
28
|
+
def message
|
29
|
+
@policy.message
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(policy)
|
34
|
+
@policy = policy
|
35
|
+
end
|
36
|
+
|
37
|
+
# The PolicyFactory interface
|
38
|
+
# Buld a Policy Runner, which is instantiated
|
39
|
+
# for each field when resolving a schema
|
40
|
+
# @param key [Symbol]
|
41
|
+
# @param value [Any]
|
42
|
+
# @option payload [Hash]
|
43
|
+
# @option context [Parametric::Context]
|
44
|
+
# @return [PolicyRunner]
|
45
|
+
def build(key, value, payload:, context:)
|
46
|
+
PolicyRunner.new(@policy, key, value, payload, context)
|
47
|
+
end
|
48
|
+
|
49
|
+
def meta_data
|
50
|
+
@policy.meta_data
|
51
|
+
end
|
52
|
+
|
53
|
+
def key
|
54
|
+
@policy.key
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
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
|
@@ -71,7 +75,9 @@ module Parametric
|
|
71
75
|
fields.each_with_object({}) do |(_, field), obj|
|
72
76
|
meta = field.meta_data.dup
|
73
77
|
sc = meta.delete(:schema)
|
74
|
-
meta[:structure] = sc.structure if sc
|
78
|
+
meta[:structure] = sc.schema.structure if sc
|
79
|
+
one_of = meta.delete(:one_of)
|
80
|
+
meta[:one_of] = one_of.values.map(&:structure) if one_of
|
75
81
|
obj[field.key] = meta
|
76
82
|
end
|
77
83
|
end
|
@@ -90,6 +96,20 @@ module Parametric
|
|
90
96
|
end
|
91
97
|
end
|
92
98
|
|
99
|
+
def before_resolve(klass = nil, &block)
|
100
|
+
raise ArgumentError, '#before_resolve expects a callable object, or a block' if !klass && !block_given?
|
101
|
+
callable = klass || block
|
102
|
+
before_hooks << callable
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
def after_resolve(klass = nil, &block)
|
107
|
+
raise ArgumentError, '#after_resolve expects a callable object, or a block' if !klass && !block_given?
|
108
|
+
callable = klass || block
|
109
|
+
after_hooks << callable
|
110
|
+
self
|
111
|
+
end
|
112
|
+
|
93
113
|
def expand(exp, &block)
|
94
114
|
expansions[exp] = block
|
95
115
|
self
|
@@ -139,19 +159,35 @@ module Parametric
|
|
139
159
|
|
140
160
|
protected
|
141
161
|
|
142
|
-
attr_reader :definitions, :options
|
162
|
+
attr_reader :definitions, :options, :before_hooks, :after_hooks
|
143
163
|
|
144
164
|
private
|
145
165
|
|
146
166
|
attr_reader :default_field_policies, :ignored_field_keys, :expansions
|
147
167
|
|
148
168
|
def coerce_one(val, context, flds: fields)
|
149
|
-
|
169
|
+
val = run_before_hooks(val, context)
|
170
|
+
|
171
|
+
out = flds.each_with_object({}) do |(_, field), m|
|
150
172
|
r = field.resolve(val, context.sub(field.key))
|
151
173
|
if r.eligible?
|
152
174
|
m[field.key] = r.value
|
153
175
|
end
|
154
176
|
end
|
177
|
+
|
178
|
+
run_after_hooks(out, context)
|
179
|
+
end
|
180
|
+
|
181
|
+
def run_before_hooks(val, context)
|
182
|
+
before_hooks.reduce(val) do |value, callable|
|
183
|
+
callable.call(value, context)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def run_after_hooks(val, context)
|
188
|
+
after_hooks.reduce(val) do |value, callable|
|
189
|
+
callable.call(value, context)
|
190
|
+
end
|
155
191
|
end
|
156
192
|
|
157
193
|
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
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parametric
|
4
|
+
# A policy that allows you to select a sub-schema based on a value in the payload.
|
5
|
+
# @example
|
6
|
+
#
|
7
|
+
# user_schema = Parametric::Schema.new do |sc, _|
|
8
|
+
# field(:name).type(:string).present
|
9
|
+
# field(:age).type(:integer).present
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# company_schema = Parametric::Schema.new do
|
13
|
+
# field(:name).type(:string).present
|
14
|
+
# field(:company_code).type(:string).present
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# schema = Parametric::Schema.new do |sc, _|
|
18
|
+
# # Use :type field to locate the sub-schema to use for :sub
|
19
|
+
# sc.field(:type).type(:string)
|
20
|
+
#
|
21
|
+
# # Use the :one_of policy to select the sub-schema based on the :type field above
|
22
|
+
# sc.field(:sub).type(:object).tagged_one_of do |sub|
|
23
|
+
# sub.index_by(:type)
|
24
|
+
# sub.on('user', user_schema)
|
25
|
+
# sub.on('company', company_schema)
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# # The schema will now select the correct sub-schema based on the value of :type
|
30
|
+
# result = schema.resolve(type: 'user', sub: { name: 'Joe', age: 30 })
|
31
|
+
#
|
32
|
+
# Instances can also be created separately and used as a policy:
|
33
|
+
# @example
|
34
|
+
#
|
35
|
+
# UserOrCompany = Parametric::TaggedOneOf.new do |sc, _|
|
36
|
+
# sc.on('user', user_schema)
|
37
|
+
# sc.on('company', company_schema)
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# schema = Parametric::Schema.new do |sc, _|
|
41
|
+
# sc.field(:type).type(:string)
|
42
|
+
# sc.field(:sub).type(:object).policy(UserOrCompany.index_by(:type))
|
43
|
+
# end
|
44
|
+
class TaggedOneOf
|
45
|
+
NOOP_INDEX = ->(payload) { payload }.freeze
|
46
|
+
def initialize(index: NOOP_INDEX, matchers: {}, &block)
|
47
|
+
@index = index
|
48
|
+
@matchers = matchers
|
49
|
+
@configuring = false
|
50
|
+
if block_given?
|
51
|
+
@configuring = true
|
52
|
+
block.call(self)
|
53
|
+
@configuring = false
|
54
|
+
end
|
55
|
+
freeze
|
56
|
+
end
|
57
|
+
|
58
|
+
def index_by(callable = nil, &block)
|
59
|
+
if callable.is_a?(Symbol)
|
60
|
+
key = callable
|
61
|
+
callable = ->(payload) { payload[key] }
|
62
|
+
end
|
63
|
+
index = callable || block
|
64
|
+
if configuring?
|
65
|
+
@index = index
|
66
|
+
else
|
67
|
+
self.class.new(index:, matchers: @matchers)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def on(key, schema)
|
72
|
+
@matchers[key] = schema
|
73
|
+
end
|
74
|
+
|
75
|
+
# The [PolicyFactory] interface
|
76
|
+
def build(key, value, payload:, context:)
|
77
|
+
Runner.new(@index, @matchers, key, value, payload, context)
|
78
|
+
end
|
79
|
+
|
80
|
+
def meta_data
|
81
|
+
{ type: :object, one_of: @matchers }
|
82
|
+
end
|
83
|
+
|
84
|
+
private def configuring?
|
85
|
+
@configuring
|
86
|
+
end
|
87
|
+
|
88
|
+
class Runner
|
89
|
+
def initialize(index, matchers, key, value, payload, context)
|
90
|
+
@matchers = matchers
|
91
|
+
@key = key
|
92
|
+
@raw_value = value
|
93
|
+
@payload = payload
|
94
|
+
@context = context
|
95
|
+
@index_value = index.call(payload)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Should this policy run at all?
|
99
|
+
# returning [false] halts the field policy chain.
|
100
|
+
# @return [Boolean]
|
101
|
+
def eligible?
|
102
|
+
true
|
103
|
+
end
|
104
|
+
|
105
|
+
# If [false], add [#message] to result errors and halt processing field.
|
106
|
+
# @return [Boolean]
|
107
|
+
def valid?
|
108
|
+
has_sub_schema?
|
109
|
+
end
|
110
|
+
|
111
|
+
# Coerce the value, or return as-is.
|
112
|
+
# @return [Any]
|
113
|
+
def value
|
114
|
+
@value ||= has_sub_schema? ? sub_schema.coerce(@raw_value, @key, @context) : @raw_value
|
115
|
+
end
|
116
|
+
|
117
|
+
# Error message for this policy
|
118
|
+
# @return [String]
|
119
|
+
def message
|
120
|
+
"#{@value} is invalid. No sub-schema found for '#{@index_value}'"
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def has_sub_schema?
|
126
|
+
@matchers.key?(@index_value)
|
127
|
+
end
|
128
|
+
|
129
|
+
def sub_schema
|
130
|
+
@sub_schema ||= @matchers[@index_value]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/parametric/version.rb
CHANGED
data/lib/parametric.rb
CHANGED
data/parametric.gemspec
CHANGED
@@ -17,8 +17,7 @@ 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
|
-
spec.add_development_dependency "rspec", '3.
|
21
|
+
spec.add_development_dependency "rspec", '3.12.0'
|
23
22
|
spec.add_development_dependency "byebug"
|
24
23
|
end
|
data/spec/field_spec.rb
CHANGED
@@ -88,6 +88,14 @@ describe Parametric::Field do
|
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
91
|
+
describe '#has_policy?' do
|
92
|
+
it 'is a boolean' do
|
93
|
+
subject.policy(:integer)
|
94
|
+
expect(subject.has_policy?(:integer)).to be(true)
|
95
|
+
expect(subject.has_policy?(:string)).to be(false)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
91
99
|
describe "#default" do
|
92
100
|
it "is default if missing key" do
|
93
101
|
resolve(subject.default("AA"), foobar: 1).tap do |r|
|
@@ -112,6 +120,20 @@ describe Parametric::Field do
|
|
112
120
|
end
|
113
121
|
end
|
114
122
|
|
123
|
+
describe '#from' do
|
124
|
+
it 'copies policies and metadata from an existing field' do
|
125
|
+
subject.policy(:string).present.options(['a', 'b', 'c'])
|
126
|
+
|
127
|
+
field = described_class.new(:another_key, registry).from(subject)
|
128
|
+
resolve(field, another_key: "abc").tap do |r|
|
129
|
+
has_errors
|
130
|
+
end
|
131
|
+
expect(field.meta_data[:type]).to eq(:string)
|
132
|
+
expect(field.meta_data[:present]).to be(true)
|
133
|
+
expect(field.meta_data[:options]).to eq(['a', 'b', 'c'])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
115
137
|
describe "#present" do
|
116
138
|
it "is valid if value is present" do
|
117
139
|
resolve(subject.present, a_key: "abc").tap do |r|
|
@@ -242,6 +264,16 @@ describe Parametric::Field do
|
|
242
264
|
end
|
243
265
|
end
|
244
266
|
|
267
|
+
describe ":value policy" do
|
268
|
+
it 'always resolves to static value' do
|
269
|
+
resolve(subject.policy(:value, 'hello'), a_key: "tag1,tag2").tap do |r|
|
270
|
+
expect(r.eligible?).to be true
|
271
|
+
no_errors
|
272
|
+
expect(r.value).to eq 'hello'
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
245
277
|
describe ":declared policy" do
|
246
278
|
it "is eligible if key exists" do
|
247
279
|
resolve(subject.policy(:declared).present, a_key: "").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/schema_spec.rb
CHANGED
@@ -251,6 +251,87 @@ describe Parametric::Schema do
|
|
251
251
|
end
|
252
252
|
end
|
253
253
|
|
254
|
+
describe '#tagged_one_of for multiple sub-schemas' do
|
255
|
+
let(:user_schema) do
|
256
|
+
described_class.new do
|
257
|
+
field(:name).type(:string).present
|
258
|
+
field(:age).type(:integer).present
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
let(:company_schema) do
|
263
|
+
described_class.new do
|
264
|
+
field(:name).type(:string).present
|
265
|
+
field(:company_code).type(:string).present
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'picks the right sub-schema' do
|
270
|
+
schema = described_class.new do |sc, _|
|
271
|
+
sc.field(:type).type(:string)
|
272
|
+
sc.field(:sub).type(:object).tagged_one_of do |sub|
|
273
|
+
sub.index_by(:type)
|
274
|
+
sub.on('user', user_schema)
|
275
|
+
sub.on('company', company_schema)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
result = schema.resolve(type: 'user', sub: { name: 'Joe', age: 30 })
|
280
|
+
expect(result.valid?).to be true
|
281
|
+
expect(result.output).to eq({ type: 'user', sub: { name: 'Joe', age: 30 } })
|
282
|
+
|
283
|
+
result = schema.resolve(type: 'company', sub: { name: 'ACME', company_code: 123 })
|
284
|
+
expect(result.valid?).to be true
|
285
|
+
expect(result.output).to eq({ type: 'company', sub: { name: 'ACME', company_code: '123' } })
|
286
|
+
|
287
|
+
result = schema.resolve(type: 'company', sub: { name: nil, company_code: 123 })
|
288
|
+
expect(result.valid?).to be false
|
289
|
+
expect(result.errors['$.sub.name']).not_to be_empty
|
290
|
+
|
291
|
+
result = schema.resolve(type: 'foo', sub: { name: 'ACME', company_code: 123 })
|
292
|
+
expect(result.valid?).to be false
|
293
|
+
end
|
294
|
+
|
295
|
+
it 'can be assigned to instance and reused' do
|
296
|
+
user_or_company = Parametric::TaggedOneOf.new do |sub|
|
297
|
+
sub.on('user', user_schema)
|
298
|
+
sub.on('company', company_schema)
|
299
|
+
end
|
300
|
+
|
301
|
+
schema = described_class.new do |sc, _|
|
302
|
+
sc.field(:type).type(:string)
|
303
|
+
sc.field(:sub).type(:object).tagged_one_of(user_or_company.index_by(:type))
|
304
|
+
end
|
305
|
+
|
306
|
+
result = schema.resolve(type: 'user', sub: { name: 'Joe', age: 30 })
|
307
|
+
expect(result.valid?).to be true
|
308
|
+
expect(result.output).to eq({ type: 'user', sub: { name: 'Joe', age: 30 } })
|
309
|
+
end
|
310
|
+
|
311
|
+
specify '#structure' do
|
312
|
+
user_or_company = Parametric::TaggedOneOf.new do |sub|
|
313
|
+
sub.on('user', user_schema)
|
314
|
+
sub.on('company', company_schema)
|
315
|
+
end
|
316
|
+
|
317
|
+
schema = described_class.new do |sc, _|
|
318
|
+
sc.field(:type).type(:string)
|
319
|
+
sc.field(:sub).type(:object).tagged_one_of(user_or_company.index_by(:type))
|
320
|
+
end
|
321
|
+
|
322
|
+
structure = schema.structure
|
323
|
+
structure.dig(:sub).tap do |sub|
|
324
|
+
expect(sub[:type]).to eq :object
|
325
|
+
expect(sub[:one_of][0][:name][:type]).to eq :string
|
326
|
+
expect(sub[:one_of][0][:name][:required]).to be true
|
327
|
+
expect(sub[:one_of][0][:name][:present]).to be true
|
328
|
+
expect(sub[:one_of][0][:age][:type]).to eq :integer
|
329
|
+
|
330
|
+
expect(sub[:one_of][1][:company_code][:type]).to eq :string
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
254
335
|
describe "#ignore" do
|
255
336
|
it "ignores fields" do
|
256
337
|
s1 = described_class.new.ignore(:title, :status) do
|