parametric 0.2.17 → 0.2.18

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 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