parametric 0.2.10 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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
|