parametric 0.2.17 → 0.2.18

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71e71a5ddf414bb333a499f425d68e891f60a2e0610a8a57d8362f6481da16e6
4
- data.tar.gz: 5c13b835147015f11f66591d311612139b16c4001891451d299c39358045725c
3
+ metadata.gz: 403aac3495b3e90bc32cf01c0229b83497d37413fb43b6180c3dafd0a9ec36b9
4
+ data.tar.gz: a8396d8ab3831e7c93d1643f96562ff0d5fe1ddaa8794dfe7ed8993bd4732cd5
5
5
  SHA512:
6
- metadata.gz: 8cd1bb404bb5bcf24fc7b4826dcf360d367efb23cbe0e1d300a25f3e0a709ee5fb4e9d17db5820803a9e8f2f0b65e68ddae5c27325573f7fc213c7814a8f4e04
7
- data.tar.gz: 0fd11bca14c7c75decb6dcc9781ef1b26a3ecc7bed3b4348b30f5c8c06ad57237b10beb7d3acd6a5b1f0e8989823da29e2f3bc3956f51856b1e57b852a7bdd5d
6
+ metadata.gz: dbd04349edf7e8f4777ef7c681ec8f16d2cf570d915088fae04942760415d9fd88123741b68957839b21d7145d97b83b6cf6911fce5f0e2db806d34a304e1a0d
7
+ data.tar.gz: 9851e0d8f4e5e48d38870027c8a987fbf65f264a8c9dfaafe74080ff463e3783267f605eae09ce65b1f43547a27935ae532fd75120d60823fd24e8bf09bd0c4d
data/README.md CHANGED
@@ -127,6 +127,60 @@ person_schema = Parametric::Schema.new do |sc, options|
127
127
  sc.field(:friends).type(:array).schema(friends_schema)
128
128
  end
129
129
  ```
130
+
131
+ ## Tagged One Of (multiple nested schemas, discriminated by payload key).
132
+
133
+ You can use `Field#tagged_one_of` to resolve a nested schema based on the value of a top-level field.
134
+
135
+ ```ruby
136
+ user_schema = Parametric::Schema.new do |sc, _|
137
+ field(:name).type(:string).present
138
+ field(:age).type(:integer).present
139
+ end
140
+
141
+ company_schema = Parametric::Schema.new do
142
+ field(:name).type(:string).present
143
+ field(:company_code).type(:string).present
144
+ end
145
+
146
+ schema = Parametric::Schema.new do |sc, _|
147
+ # Use :type field to locate the sub-schema to use for :sub
148
+ sc.field(:type).type(:string)
149
+
150
+ # Use the :one_of policy to select the sub-schema based on the :type field above
151
+ sc.field(:sub).type(:object).tagged_one_of do |sub|
152
+ sub.index_by(:type)
153
+ sub.on('user', user_schema)
154
+ sub.on('company', company_schema)
155
+ end
156
+ end
157
+
158
+ # The schema will now select the correct sub-schema based on the value of :type
159
+ result = schema.resolve(type: 'user', sub: { name: 'Joe', age: 30 })
160
+
161
+ # Instances can also be created separately and used as a policy:
162
+
163
+ UserOrCompany = Parametric::TaggedOneOf.new do |sc, _|
164
+ sc.on('user', user_schema)
165
+ sc.on('company', company_schema)
166
+ end
167
+
168
+ schema = Parametric::Schema.new do |sc, _|
169
+ sc.field(:type).type(:string)
170
+ sc.field(:sub).type(:object).policy(UserOrCompany.index_by(:type))
171
+ end
172
+ ```
173
+
174
+ `#index_by` can take a block to decide what value to resolve schemas by:
175
+
176
+ ```ruby
177
+ sc.field(:sub).type(:object).tagged_one_of do |sub|
178
+ sub.index_by { |payload| payload[:entity_type] }
179
+ sub.on('user', user_schema)
180
+ sub.on('company', company_schema)
181
+ end
182
+ ```
183
+
130
184
  ## Built-in policies
131
185
 
132
186
  Type coercions (the `type` method) and validations (the `validate` method) are all _policies_.
@@ -285,60 +339,96 @@ field(:currency).policy(:value, 'gbp') # this field always resolves to 'gbp'
285
339
 
286
340
  ## Custom policies
287
341
 
288
- You can also register your own custom policy objects. A policy must implement the following methods:
342
+ You can also register your own custom policy objects.
343
+ A policy consist of the following:
344
+
345
+ * A `PolicyFactory` interface:
289
346
 
290
347
  ```ruby
291
348
  class MyPolicy
292
- # Validation error message, if invalid
293
- def message
294
- 'is invalid'
349
+ # Initializer signature is up to you.
350
+ # These are the arguments passed to the policy when using in a Field,
351
+ # ex. field(:name).policy(:my_policy, 'arg1', 'arg2')
352
+ def initialize(arg1, arg2)
353
+ @arg1, @arg2 = arg1, arg2
295
354
  end
296
355
 
297
- # Whether or not to validate and coerce this value
298
- # if false, no other policies will be run on the field
299
- def eligible?(value, key, payload)
300
- true
356
+ # @return [Hash]
357
+ def meta_data
358
+ { type: :string }
301
359
  end
302
360
 
303
- # Transform the value
304
- def coerce(value, key, context)
305
- value
361
+ # Buld a Policy Runner, which is instantiated
362
+ # for each field when resolving a schema
363
+ # @param key [Symbol]
364
+ # @param value [Any]
365
+ # @option payload [Hash]
366
+ # @option context [Parametric::Context]
367
+ # @return [PolicyRunner]
368
+ def build(key, value, payload:, context:)
369
+ MyPolicyRunner.new(key, value, payload, context)
370
+ end
371
+ end
372
+ ```
373
+
374
+ * A `PolicyRunner` interface.
375
+
376
+ ```ruby
377
+ class MyPolicyRunner
378
+ # Initializer is up to you. See `MyPolicy#build`
379
+ def initialize(key, value, payload, context)
380
+
306
381
  end
307
382
 
308
- # Is the value valid?
309
- def valid?(value, key, payload)
383
+ # Should this policy run at all?
384
+ # returning [false] halts the field policy chain.
385
+ # @return [Boolean]
386
+ def eligible?
310
387
  true
311
388
  end
312
389
 
313
- # merge this object into the field's meta data
314
- def meta_data
315
- {type: :string}
390
+ # If [false], add [#message] to result errors and halt processing field.
391
+ # @return [Boolean]
392
+ def valid?
393
+ true
394
+ end
395
+
396
+ # Coerce the value, or return as-is.
397
+ # @return [Any]
398
+ def value
399
+ @value
400
+ end
401
+
402
+ # Error message for this policy
403
+ # @return [String]
404
+ def message
405
+ "#{@value} is invalid"
316
406
  end
317
407
  end
318
408
  ```
319
409
 
320
- You can register your policy with:
410
+ Then register your custom policy factory:
321
411
 
322
412
  ```ruby
323
- Parametric.policy :my_policy, MyPolicy
413
+ Parametric.policy :my_polict, MyPolicy
324
414
  ```
325
415
 
326
416
  And then refer to it by name when declaring your schema fields
327
417
 
328
418
  ```ruby
329
- field(:title).policy(:my_policy)
419
+ field(:title).policy(:my_policy, 'arg1', 'arg2')
330
420
  ```
331
421
 
332
422
  You can chain custom policies with other policies.
333
423
 
334
424
  ```ruby
335
- field(:title).required.policy(:my_policy)
425
+ field(:title).required.policy(:my_policy, 'arg1', 'arg2')
336
426
  ```
337
427
 
338
428
  Note that you can also register instances.
339
429
 
340
430
  ```ruby
341
- Parametric.policy :my_policy, MyPolicy.new
431
+ Parametric.policy :my_policy, MyPolicy.new('arg1', 'arg2')
342
432
  ```
343
433
 
344
434
  For example, a policy that can be configured on a field-by-field basis:
@@ -349,27 +439,34 @@ class AddJobTitle
349
439
  @job_title = job_title
350
440
  end
351
441
 
352
- def message
353
- 'is invalid'
442
+ def build(key, value, payload:, context:)
443
+ Runner.new(@job_title, key, value, payload, context)
354
444
  end
355
445
 
356
- # Noop
357
- def eligible?(value, key, payload)
358
- true
446
+ def meta_data
447
+ {}
359
448
  end
360
449
 
361
- # Add job title to value
362
- def coerce(value, key, context)
363
- "#{value}, #{@job_title}"
364
- end
450
+ class Runner
451
+ attr_reader :message
365
452
 
366
- # Noop
367
- def valid?(value, key, payload)
368
- true
369
- end
453
+ def initialize(job_title, key, value, payload, _context)
454
+ @job_title = job_title
455
+ @key, @value, @payload = key, value, payload
456
+ @message = 'is invalid'
457
+ end
370
458
 
371
- def meta_data
372
- {}
459
+ def eligible?
460
+ true
461
+ end
462
+
463
+ def valid?
464
+ true
465
+ end
466
+
467
+ def value
468
+ "#{@value}, #{@job_title}"
469
+ end
373
470
  end
374
471
  end
375
472
 
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'delegate'
4
- require "parametric/field_dsl"
4
+ require 'parametric/field_dsl'
5
+ require 'parametric/policy_adapter'
6
+ require 'parametric/tagged_one_of'
5
7
 
6
8
  module Parametric
7
9
  class ConfigurationError < StandardError; end
@@ -40,6 +42,10 @@ module Parametric
40
42
  end
41
43
  alias_method :type, :policy
42
44
 
45
+ def tagged_one_of(instance = nil, &block)
46
+ policy(instance || Parametric::TaggedOneOf.new(&block))
47
+ end
48
+
43
49
  def schema(sc = nil, &block)
44
50
  sc = (sc ? sc : Schema.new(&block))
45
51
  meta schema: sc
@@ -79,20 +85,26 @@ module Parametric
79
85
  end
80
86
 
81
87
  policies.each do |policy|
82
- if !policy.eligible?(value, key, payload)
83
- eligible = false
84
- if has_default?
85
- eligible = true
86
- value = default_block.call(key, payload, context)
88
+ begin
89
+ pol = policy.build(key, value, payload:, context:)
90
+ if !pol.eligible?
91
+ eligible = false
92
+ if has_default?
93
+ eligible = true
94
+ value = default_block.call(key, payload, context)
95
+ end
96
+ break
97
+ else
98
+ value = pol.value
99
+ if !pol.valid?
100
+ eligible = true # eligible, but has errors
101
+ context.add_error pol.message
102
+ break # only one error at a time
103
+ end
87
104
  end
105
+ rescue StandardError => e
106
+ context.add_error e.message
88
107
  break
89
- else
90
- value = resolve_one(policy, value, context)
91
- if !policy.valid?(value, key, payload)
92
- eligible = true # eligible, but has errors
93
- context.add_error policy.message
94
- break # only one error at a time
95
- end
96
108
  end
97
109
  end
98
110
 
@@ -107,15 +119,6 @@ module Parametric
107
119
 
108
120
  attr_reader :registry, :default_block
109
121
 
110
- def resolve_one(policy, value, context)
111
- begin
112
- policy.coerce(value, key, context)
113
- rescue StandardError => e
114
- context.add_error e.message
115
- value
116
- end
117
- end
118
-
119
122
  def has_default?
120
123
  !!default_block && !meta_data[:skip_default]
121
124
  end
@@ -127,6 +130,7 @@ module Parametric
127
130
 
128
131
  obj = obj.new(*args) if obj.respond_to?(:new)
129
132
  obj = PolicyWithKey.new(obj, key)
133
+ obj = PolicyAdapter.new(obj) unless obj.respond_to?(:build)
130
134
 
131
135
  obj
132
136
  end
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Parametric
4
- VERSION = '0.2.17'
4
+ VERSION = '0.2.18'
5
5
  end
data/parametric.gemspec CHANGED
@@ -18,6 +18,6 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ["lib"]
19
19
 
20
20
  spec.add_development_dependency "rake"
21
- spec.add_development_dependency "rspec", '3.4.0'
21
+ spec.add_development_dependency "rspec", '3.12.0'
22
22
  spec.add_development_dependency "byebug"
23
23
  end
data/spec/schema_spec.rb CHANGED
@@ -251,6 +251,64 @@ 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
+ end
311
+
254
312
  describe "#ignore" do
255
313
  it "ignores fields" do
256
314
  s1 = described_class.new.ignore(:title, :status) do
data/spec/struct_spec.rb CHANGED
@@ -301,6 +301,9 @@ describe Parametric::Struct do
301
301
 
302
302
  schema do
303
303
  field(:title).type(:string).present
304
+ field(:consumption).type(:object).present.schema do
305
+ field(:type).present.options(%w[aaa bbb])
306
+ end
304
307
  end
305
308
  end
306
309
 
@@ -308,9 +311,10 @@ describe Parametric::Struct do
308
311
  klass.new!(title: '')
309
312
  rescue Parametric::InvalidStructError => e
310
313
  expect(e.errors['$.title']).not_to be nil
314
+ expect(e.errors['$.consumption']).not_to be nil
311
315
  end
312
316
 
313
- valid = klass.new!(title: 'foo')
317
+ valid = klass.new!(title: 'foo', consumption: { type: 'aaa' })
314
318
  expect(valid.title).to eq 'foo'
315
319
  end
316
320
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parametric
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.17
4
+ version: 0.2.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-29 00:00:00.000000000 Z
11
+ date: 2023-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - '='
32
32
  - !ruby/object:Gem::Version
33
- version: 3.4.0
33
+ version: 3.12.0
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - '='
39
39
  - !ruby/object:Gem::Version
40
- version: 3.4.0
40
+ version: 3.12.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: byebug
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -77,10 +77,12 @@ files:
77
77
  - lib/parametric/field.rb
78
78
  - lib/parametric/field_dsl.rb
79
79
  - lib/parametric/policies.rb
80
+ - lib/parametric/policy_adapter.rb
80
81
  - lib/parametric/registry.rb
81
82
  - lib/parametric/results.rb
82
83
  - lib/parametric/schema.rb
83
84
  - lib/parametric/struct.rb
85
+ - lib/parametric/tagged_one_of.rb
84
86
  - lib/parametric/version.rb
85
87
  - parametric.gemspec
86
88
  - spec/custom_block_validator_spec.rb
@@ -113,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
115
  - !ruby/object:Gem::Version
114
116
  version: '0'
115
117
  requirements: []
116
- rubygems_version: 3.2.32
118
+ rubygems_version: 3.4.18
117
119
  signing_key:
118
120
  specification_version: 4
119
121
  summary: DSL for declaring allowed parameters with options, regexp patern and default