dry-schema 1.3.2 → 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  [gem]: https://rubygems.org/gems/dry-schema
2
- [travis]: https://travis-ci.com/dry-rb/dry-schema
2
+ [actions]: https://github.com/dry-rb/dry-schema/actions
3
3
  [codeclimate]: https://codeclimate.com/github/dry-rb/dry-schema
4
4
  [chat]: https://dry-rb.zulipchat.com
5
5
  [inchpages]: http://inch-ci.org/github/dry-rb/dry-schema
@@ -7,7 +7,7 @@
7
7
  # dry-schema [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
8
8
 
9
9
  [![Gem Version](https://badge.fury.io/rb/dry-schema.svg)][gem]
10
- [![Build Status](https://travis-ci.com/dry-rb/dry-schema.svg?branch=master)][travis]
10
+ [![CI Status](https://github.com/dry-rb/dry-schema/workflows/ci/badge.svg)][actions]
11
11
  [![Code Climate](https://codeclimate.com/github/dry-rb/dry-schema/badges/gpa.svg)][codeclimate]
12
12
  [![Test Coverage](https://codeclimate.com/github/dry-rb/dry-schema/badges/coverage.svg)][codeclimate]
13
13
  [![Inline docs](http://inch-ci.org/github/dry-rb/dry-schema.svg?branch=master)][inchpages]
@@ -30,7 +30,7 @@ module Dry
30
30
  #
31
31
  # @api public
32
32
  def self.define(**options, &block)
33
- DSL.new(options, &block).call
33
+ DSL.new(**options, &block).call
34
34
  end
35
35
 
36
36
  # Define a schema suitable for HTTP params
@@ -26,10 +26,11 @@ module Dry
26
26
  # An error raised when a localized message cannot be found
27
27
  MissingMessageError = Class.new(StandardError) do
28
28
  # @api private
29
- def initialize(path)
29
+ def initialize(path, paths = [])
30
30
  *rest, rule = path
31
31
  super(<<~STR)
32
- Message template for #{rule.inspect} under #{rest.join(DOT).inspect} was not found
32
+ Message template for #{rule.inspect} under #{rest.join(DOT).inspect} was not found. Searched in:
33
+ #{paths.map { |string| "\"#{string}\"" }.join("\n")}
33
34
  STR
34
35
  end
35
36
  end
@@ -9,6 +9,7 @@ require 'dry/schema/types'
9
9
  require 'dry/schema/macros'
10
10
 
11
11
  require 'dry/schema/processor'
12
+ require 'dry/schema/processor_steps'
12
13
  require 'dry/schema/key_map'
13
14
  require 'dry/schema/key_coercer'
14
15
  require 'dry/schema/value_coercer'
@@ -50,8 +51,6 @@ module Dry
50
51
 
51
52
  extend Dry::Initializer
52
53
 
53
- include ::Dry::Equalizer(:options)
54
-
55
54
  # @return [Compiler] The rule compiler object
56
55
  option :compiler, default: -> { Compiler.new }
57
56
 
@@ -64,18 +63,21 @@ module Dry
64
63
  # @return [Compiler] A key=>type map defined within the DSL
65
64
  option :types, default: -> { EMPTY_HASH.dup }
66
65
 
67
- # @return [DSL] An optional parent DSL object that will be used to merge keys and rules
68
- option :parent, optional: true
66
+ # @return [Array] Optional parent DSL objects, that will be used to merge keys and rules
67
+ option :parent, Types::Coercible::Array, default: -> { EMPTY_ARRAY.dup }, as: :parents
69
68
 
70
69
  # @return [Config] Configuration object exposed via `#configure` method
71
70
  option :config, optional: true, default: proc { parent ? parent.config.dup : Config.new }
72
71
 
72
+ # @return [ProcessorSteps] Steps for the processor
73
+ option :steps, default: proc { parent ? parent.steps.dup : ProcessorSteps.new }
74
+
73
75
  # Build a new DSL object and evaluate provided block
74
76
  #
75
77
  # @param [Hash] options
76
78
  # @option options [Class] :processor The processor type (`Params`, `JSON` or a custom sub-class)
77
79
  # @option options [Compiler] :compiler An instance of a rule compiler (must be compatible with `Schema::Compiler`) (optional)
78
- # @option options [DSL] :parent An instance of the parent DSL (optional)
80
+ # @option options [Array[DSL]] :parent One or more instances of the parent DSL (optional)
79
81
  # @option options [Config] :config A configuration object (optional)
80
82
  #
81
83
  # @see Schema.define
@@ -189,9 +191,10 @@ module Dry
189
191
  #
190
192
  # @api private
191
193
  def call
192
- steps = [key_coercer]
193
- steps << filter_schema.rule_applier if filter_rules?
194
- steps << value_coercer << rule_applier
194
+ steps[:key_coercer] = key_coercer
195
+ steps[:value_coercer] = value_coercer
196
+ steps[:rule_applier] = rule_applier
197
+ steps[:filter_schema] = filter_schema.rule_applier if filter_rules?
195
198
 
196
199
  processor_type.new(schema_dsl: self, steps: steps)
197
200
  end
@@ -215,14 +218,54 @@ module Dry
215
218
  -> member_type { type_registry['array'].of(resolve_type(member_type)) }
216
219
  end
217
220
 
221
+ # Method allows steps injection to the processor
222
+ #
223
+ # @example
224
+ # before(:rule_applier) do |input|
225
+ # input.compact
226
+ # end
227
+ #
228
+ # @return [DSL]
229
+ #
230
+ # @api public
231
+ def before(key, &block)
232
+ steps.before(key, &block)
233
+ self
234
+ end
235
+
236
+ # Method allows steps injection to the processor
237
+ #
238
+ # @example
239
+ # after(:rule_applier) do |input|
240
+ # input.compact
241
+ # end
242
+ #
243
+ # @return [DSL]
244
+ #
245
+ # @api public
246
+ def after(key, &block)
247
+ steps.after(key, &block)
248
+ self
249
+ end
250
+
251
+ # The parent (last from parents) which is used for copying non mergeable configuration
252
+ #
253
+ # @return DSL
254
+ #
255
+ # @api public
256
+ def parent
257
+ @parent ||= parents.last
258
+ end
259
+
218
260
  # Return type schema used by the value coercer
219
261
  #
220
262
  # @return [Dry::Types::Safe]
221
263
  #
222
264
  # @api private
223
265
  def type_schema
224
- schema = type_registry['hash'].schema(types).lax
225
- parent ? parent.type_schema.schema(schema.to_a) : schema
266
+ our_schema = type_registry['hash'].schema(types).lax
267
+ schemas = [*parents.map(&:type_schema), our_schema]
268
+ schemas.inject { |result, schema| result.schema(schema.to_a) }
226
269
  end
227
270
 
228
271
  # Return a new DSL instance using the same processor type
@@ -230,8 +273,8 @@ module Dry
230
273
  # @return [Dry::Types::Safe]
231
274
  #
232
275
  # @api private
233
- def new(options = EMPTY_HASH, &block)
234
- self.class.new(options.merge(processor_type: processor_type, config: config), &block)
276
+ def new(**options, &block)
277
+ self.class.new(**options, processor_type: processor_type, config: config, &block)
235
278
  end
236
279
 
237
280
  # Set a type for the given key name
@@ -276,14 +319,18 @@ module Dry
276
319
  #
277
320
  # @api private
278
321
  def filter_schema_dsl
279
- @filter_schema_dsl ||= new(parent: parent_filter_schema)
322
+ @filter_schema_dsl ||= new(parent: parent_filter_schemas)
280
323
  end
281
324
 
282
325
  # Check if any filter rules were defined
283
326
  #
284
327
  # @api private
285
328
  def filter_rules?
286
- (instance_variable_defined?('@filter_schema_dsl') && !filter_schema_dsl.macros.empty?) || parent&.filter_rules?
329
+ if instance_variable_defined?('@filter_schema_dsl') && !filter_schema_dsl.macros.empty?
330
+ return true
331
+ end
332
+
333
+ parents.any?(&:filter_rules?)
287
334
  end
288
335
 
289
336
  protected
@@ -323,10 +370,8 @@ module Dry
323
370
  private
324
371
 
325
372
  # @api private
326
- def parent_filter_schema
327
- return unless parent
328
-
329
- parent.filter_schema if parent.filter_rules?
373
+ def parent_filter_schemas
374
+ parents.select(&:filter_rules?).map(&:filter_schema)
330
375
  end
331
376
 
332
377
  # Build a key coercer
@@ -380,12 +425,12 @@ module Dry
380
425
 
381
426
  # @api private
382
427
  def parent_rules
383
- parent&.rules || EMPTY_HASH
428
+ parents.reduce({}) { |rules, parent| rules.merge(parent.rules) }
384
429
  end
385
430
 
386
431
  # @api private
387
432
  def parent_key_map
388
- parent&.key_map || EMPTY_ARRAY
433
+ parents.reduce([]) { |key_map, parent| parent.key_map + key_map }
389
434
  end
390
435
  end
391
436
  end
@@ -19,7 +19,7 @@ module Dry
19
19
  attr_reader :hints
20
20
 
21
21
  # @api private
22
- def initialize(*args)
22
+ def initialize(*, **)
23
23
  super
24
24
  @hints = @options.fetch(:hints, true)
25
25
  end
@@ -27,8 +27,8 @@ module Dry
27
27
  end
28
28
 
29
29
  # @api private
30
- def self.new(*args)
31
- fetch_or_store(*args) { super }
30
+ def self.new(*args, **kwargs)
31
+ fetch_or_store(args, kwargs) { super }
32
32
  end
33
33
 
34
34
  # @api private
@@ -65,8 +65,8 @@ module Dry
65
65
  end
66
66
 
67
67
  # @api private
68
- def new(new_opts = EMPTY_HASH)
69
- self.class.new(id, { name: name, coercer: coercer }.merge(new_opts))
68
+ def new(**new_opts)
69
+ self.class.new(id, name: name, coercer: coercer, **new_opts)
70
70
  end
71
71
 
72
72
  # @api private
@@ -28,8 +28,8 @@ module Dry
28
28
  option :schema_dsl, optional: true
29
29
 
30
30
  # @api private
31
- def new(options = EMPTY_HASH)
32
- self.class.new({ name: name, compiler: compiler, schema_dsl: schema_dsl }.merge(options))
31
+ def new(**options)
32
+ self.class.new(name: name, compiler: compiler, schema_dsl: schema_dsl, **options)
33
33
  end
34
34
 
35
35
  # @api private
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dry/logic/operators'
4
+ require 'dry/types/predicate_inferrer'
5
+ require 'dry/types/primitive_inferrer'
4
6
 
5
7
  require 'dry/schema/macros/core'
6
8
 
@@ -26,7 +28,13 @@ module Dry
26
28
  # PredicateInferrer is used to infer predicate type-check from a type spec
27
29
  # @return [PredicateInferrer]
28
30
  # @api private
29
- option :predicate_inferrer, default: proc { PredicateInferrer.new(compiler.predicates) }
31
+ option :predicate_inferrer, default: proc { ::Dry::Types::PredicateInferrer.new(compiler.predicates) }
32
+
33
+ # @!attribute [r] primitive_inferrer
34
+ # PrimitiveInferrer used to get a list of primitive classes from configured type
35
+ # @return [PrimitiveInferrer]
36
+ # @api private
37
+ option :primitive_inferrer, default: proc { ::Dry::Types::PrimitiveInferrer.new }
30
38
 
31
39
  # @overload value(*predicates, **predicate_opts)
32
40
  # Set predicates without and with arguments
@@ -49,11 +57,12 @@ module Dry
49
57
  # @return [Macros::Core]
50
58
  #
51
59
  # @api public
52
- def value(*predicates, **opts, &block)
60
+ def value(*predicates, &block)
53
61
  append_macro(Macros::Value) do |macro|
54
- macro.call(*predicates, **opts, &block)
62
+ macro.call(*predicates, &block)
55
63
  end
56
64
  end
65
+ ruby2_keywords :value if respond_to?(:ruby2_keywords, true)
57
66
 
58
67
  # Prepends `:filled?` predicate
59
68
  #
@@ -66,11 +75,12 @@ module Dry
66
75
  # @return [Macros::Core]
67
76
  #
68
77
  # @api public
69
- def filled(*args, **opts, &block)
78
+ def filled(*args, &block)
70
79
  append_macro(Macros::Filled) do |macro|
71
- macro.call(*args, **opts, &block)
80
+ macro.call(*args, &block)
72
81
  end
73
82
  end
83
+ ruby2_keywords :filled if respond_to?(:ruby2_keywords, true)
74
84
 
75
85
  # Specify a nested hash without enforced `hash?` type-check
76
86
  #
@@ -91,6 +101,7 @@ module Dry
91
101
  macro.call(*args, &block)
92
102
  end
93
103
  end
104
+ ruby2_keywords :schema if respond_to?(:ruby2_keywords, true)
94
105
 
95
106
  # Specify a nested hash with enforced `hash?` type-check
96
107
  #
@@ -105,6 +116,7 @@ module Dry
105
116
  macro.call(*args, &block)
106
117
  end
107
118
  end
119
+ ruby2_keywords :hash if respond_to?(:ruby2_keywords, true)
108
120
 
109
121
  # Specify predicates that should be applied to each element of an array
110
122
  #
@@ -128,6 +140,7 @@ module Dry
128
140
  macro.value(*args, &block)
129
141
  end
130
142
  end
143
+ ruby2_keywords :each if respond_to?(:ruby2_keywords, true)
131
144
 
132
145
  # Like `each` but sets `array?` type-check
133
146
  #
@@ -147,6 +160,7 @@ module Dry
147
160
  macro.value(*args, &block)
148
161
  end
149
162
  end
163
+ ruby2_keywords :array if respond_to?(:ruby2_keywords, true)
150
164
 
151
165
  # Set type spec
152
166
  #
@@ -192,9 +206,7 @@ module Dry
192
206
  predicates = Array(type_spec ? args[1..-1] : args)
193
207
 
194
208
  if type_spec
195
- resolved_type = schema_dsl.resolve_type(
196
- nullable && !type_spec.is_a?(::Array) ? [:nil, type_spec] : type_spec
197
- )
209
+ resolved_type = resolve_type(type_spec, nullable)
198
210
 
199
211
  type(resolved_type) if set_type
200
212
 
@@ -207,6 +219,17 @@ module Dry
207
219
 
208
220
  yield(*predicates, type_spec: type_spec)
209
221
  end
222
+
223
+ # @api private
224
+ def resolve_type(type_spec, nullable)
225
+ resolved = schema_dsl.resolve_type(type_spec)
226
+
227
+ if type_spec.is_a?(::Array) || !nullable || resolved.optional?
228
+ resolved
229
+ else
230
+ schema_dsl.resolve_type([:nil, resolved])
231
+ end
232
+ end
210
233
  end
211
234
  end
212
235
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/primitive_inferrer'
4
3
  require 'dry/schema/macros/value'
5
4
 
6
5
  module Dry
@@ -10,12 +9,6 @@ module Dry
10
9
  #
11
10
  # @api private
12
11
  class Filled < Value
13
- # @!attribute [r] primitive_inferrer
14
- # PrimitiveInferrer used to get a list of primitive classes from configured type
15
- # @return [PrimitiveInferrer]
16
- # @api private
17
- option :primitive_inferrer, default: proc { PrimitiveInferrer.new }
18
-
19
12
  # @api private
20
13
  def call(*predicates, **opts, &block)
21
14
  ensure_valid_predicates(predicates)
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/predicate_inferrer'
4
3
  require 'dry/schema/processor'
5
4
  require 'dry/schema/macros/dsl'
6
5
  require 'dry/schema/constants'
@@ -31,6 +30,7 @@ module Dry
31
30
  (filter_schema_dsl[name] || filter_schema_dsl.optional(name)).value(*args, &block)
32
31
  self
33
32
  end
33
+ ruby2_keywords(:filter) if respond_to?(:ruby2_keywords, true)
34
34
 
35
35
  # @overload value(type_spec, *predicates, **predicate_opts)
36
36
  # Set type specification and predicates
@@ -88,7 +88,7 @@ module Dry
88
88
  def maybe(*args, **opts, &block)
89
89
  extract_type_spec(*args, nullable: true) do |*predicates, type_spec:|
90
90
  append_macro(Macros::Maybe) do |macro|
91
- macro.call(*predicates, **opts, &block)
91
+ macro.call(*predicates, type_spec: type_spec, **opts, &block)
92
92
  end
93
93
  end
94
94
  end
@@ -28,8 +28,8 @@ module Dry
28
28
  definition = schema_dsl.new(&block)
29
29
  schema = definition.call
30
30
  type_schema =
31
- if array?
32
- parent_type.of(definition.type_schema)
31
+ if array_type?(parent_type)
32
+ build_array_type(parent_type, definition.type_schema)
33
33
  elsif redefined_schema?(args)
34
34
  parent_type.schema(definition.types)
35
35
  else
@@ -56,11 +56,6 @@ module Dry
56
56
  parent_type.optional?
57
57
  end
58
58
 
59
- # @api private
60
- def array?
61
- parent_type.respond_to?(:of)
62
- end
63
-
64
59
  # @api private
65
60
  def schema?
66
61
  parent_type.respond_to?(:schema)
@@ -17,8 +17,8 @@ module Dry
17
17
  current_type = schema_dsl.types[name]
18
18
 
19
19
  updated_type =
20
- if current_type.respond_to?(:of)
21
- current_type.of(schema.type_schema)
20
+ if array_type?(current_type)
21
+ build_array_type(current_type, schema.type_schema)
22
22
  else
23
23
  schema.type_schema
24
24
  end
@@ -39,6 +39,25 @@ module Dry
39
39
  self
40
40
  end
41
41
 
42
+ # @api private
43
+ def array_type?(type)
44
+ primitive_inferrer[type].eql?([::Array])
45
+ end
46
+
47
+ # @api private
48
+ def build_array_type(array_type, member)
49
+ if array_type.respond_to?(:of)
50
+ array_type.of(member)
51
+ else
52
+ raise ArgumentError, <<~ERROR.split("\n").join(' ')
53
+ Cannot define schema for a nominal array type.
54
+ Array types must be instances of Dry::Types::Array,
55
+ usually constructed with Types::Constructor(Array) { ... } or
56
+ Dry::Types['array'].constructor { ... }
57
+ ERROR
58
+ end
59
+ end
60
+
42
61
  # @api private
43
62
  def respond_to_missing?(meth, include_private = false)
44
63
  super || meth.to_s.end_with?(QUESTION_MARK)