dry-schema 1.3.4 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +253 -101
  3. data/LICENSE +1 -1
  4. data/README.md +6 -6
  5. data/config/errors.yml +4 -0
  6. data/dry-schema.gemspec +46 -0
  7. data/lib/dry-schema.rb +1 -1
  8. data/lib/dry/schema.rb +20 -7
  9. data/lib/dry/schema/compiler.rb +4 -4
  10. data/lib/dry/schema/config.rb +15 -6
  11. data/lib/dry/schema/constants.rb +19 -9
  12. data/lib/dry/schema/dsl.rb +144 -38
  13. data/lib/dry/schema/extensions.rb +10 -2
  14. data/lib/dry/schema/extensions/hints.rb +15 -8
  15. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +2 -2
  16. data/lib/dry/schema/extensions/hints/message_set_methods.rb +0 -47
  17. data/lib/dry/schema/extensions/info.rb +27 -0
  18. data/lib/dry/schema/extensions/info/schema_compiler.rb +105 -0
  19. data/lib/dry/schema/extensions/monads.rb +1 -1
  20. data/lib/dry/schema/extensions/struct.rb +32 -0
  21. data/lib/dry/schema/json.rb +1 -1
  22. data/lib/dry/schema/key.rb +20 -5
  23. data/lib/dry/schema/key_coercer.rb +4 -4
  24. data/lib/dry/schema/key_map.rb +9 -4
  25. data/lib/dry/schema/key_validator.rb +66 -0
  26. data/lib/dry/schema/macros.rb +8 -8
  27. data/lib/dry/schema/macros/array.rb +17 -4
  28. data/lib/dry/schema/macros/core.rb +11 -6
  29. data/lib/dry/schema/macros/dsl.rb +53 -21
  30. data/lib/dry/schema/macros/each.rb +4 -4
  31. data/lib/dry/schema/macros/filled.rb +5 -6
  32. data/lib/dry/schema/macros/hash.rb +21 -3
  33. data/lib/dry/schema/macros/key.rb +10 -10
  34. data/lib/dry/schema/macros/maybe.rb +4 -5
  35. data/lib/dry/schema/macros/optional.rb +1 -1
  36. data/lib/dry/schema/macros/required.rb +1 -1
  37. data/lib/dry/schema/macros/schema.rb +23 -2
  38. data/lib/dry/schema/macros/value.rb +34 -7
  39. data/lib/dry/schema/message.rb +35 -9
  40. data/lib/dry/schema/message/or.rb +18 -39
  41. data/lib/dry/schema/message/or/abstract.rb +28 -0
  42. data/lib/dry/schema/message/or/multi_path.rb +37 -0
  43. data/lib/dry/schema/message/or/single_path.rb +64 -0
  44. data/lib/dry/schema/message_compiler.rb +40 -19
  45. data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
  46. data/lib/dry/schema/message_set.rb +26 -37
  47. data/lib/dry/schema/messages.rb +6 -6
  48. data/lib/dry/schema/messages/abstract.rb +79 -66
  49. data/lib/dry/schema/messages/i18n.rb +36 -10
  50. data/lib/dry/schema/messages/namespaced.rb +13 -3
  51. data/lib/dry/schema/messages/template.rb +19 -44
  52. data/lib/dry/schema/messages/yaml.rb +72 -13
  53. data/lib/dry/schema/params.rb +1 -1
  54. data/lib/dry/schema/path.rb +44 -5
  55. data/lib/dry/schema/predicate.rb +2 -2
  56. data/lib/dry/schema/predicate_inferrer.rb +4 -184
  57. data/lib/dry/schema/predicate_registry.rb +3 -24
  58. data/lib/dry/schema/primitive_inferrer.rb +3 -86
  59. data/lib/dry/schema/processor.rb +54 -50
  60. data/lib/dry/schema/processor_steps.rb +139 -0
  61. data/lib/dry/schema/result.rb +52 -5
  62. data/lib/dry/schema/rule_applier.rb +8 -7
  63. data/lib/dry/schema/step.rb +79 -0
  64. data/lib/dry/schema/trace.rb +5 -4
  65. data/lib/dry/schema/type_container.rb +3 -3
  66. data/lib/dry/schema/type_registry.rb +2 -2
  67. data/lib/dry/schema/types.rb +1 -1
  68. data/lib/dry/schema/value_coercer.rb +2 -2
  69. data/lib/dry/schema/version.rb +1 -1
  70. metadata +21 -7
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2019 dry-rb team
3
+ Copyright (c) 2015-2020 dry-rb team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  [gem]: https://rubygems.org/gems/dry-schema
2
- [travis]: https://travis-ci.com/dry-rb/dry-schema
3
- [codeclimate]: https://codeclimate.com/github/dry-rb/dry-schema
2
+ [actions]: https://github.com/dry-rb/dry-schema/actions
3
+ [codacy]: https://www.codacy.com/gh/dry-rb/dry-schema
4
4
  [chat]: https://dry-rb.zulipchat.com
5
5
  [inchpages]: http://inch-ci.org/github/dry-rb/dry-schema
6
6
 
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]
11
- [![Code Climate](https://codeclimate.com/github/dry-rb/dry-schema/badges/gpa.svg)][codeclimate]
12
- [![Test Coverage](https://codeclimate.com/github/dry-rb/dry-schema/badges/coverage.svg)][codeclimate]
10
+ [![CI Status](https://github.com/dry-rb/dry-schema/workflows/ci/badge.svg)][actions]
11
+ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/961f5c776f1d49218b2cede3745e059c)][codacy]
12
+ [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/961f5c776f1d49218b2cede3745e059c)][codacy]
13
13
  [![Inline docs](http://inch-ci.org/github/dry-rb/dry-schema.svg?branch=master)][inchpages]
14
14
 
15
15
  ## Links
@@ -19,7 +19,7 @@
19
19
 
20
20
  ## Supported Ruby versions
21
21
 
22
- This library officially supports following Ruby versions:
22
+ This library officially supports the following Ruby versions:
23
23
 
24
24
  * MRI >= `2.4`
25
25
  * jruby >= `9.2`
@@ -3,6 +3,8 @@ en:
3
3
  or: "or"
4
4
 
5
5
  errors:
6
+ unexpected_key: "is not allowed"
7
+
6
8
  array?: "must be an array"
7
9
 
8
10
  empty?: "must be empty"
@@ -101,5 +103,7 @@ en:
101
103
  default: "must be %{size} bytes long"
102
104
  range: "must be within %{size_left} - %{size_right} bytes long"
103
105
 
106
+ uuid_v4?: "is not a valid UUID"
107
+
104
108
  not:
105
109
  empty?: "cannot be empty"
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+ # this file is managed by dry-rb/devtools project
3
+
4
+ lib = File.expand_path('lib', __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'dry/schema/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'dry-schema'
10
+ spec.authors = ["Piotr Solnica"]
11
+ spec.email = ["piotr.solnica@gmail.com"]
12
+ spec.license = 'MIT'
13
+ spec.version = Dry::Schema::VERSION.dup
14
+
15
+ spec.summary = "Coercion and validation for data structures"
16
+ spec.description = <<~TEXT
17
+ dry-schema provides a DSL for defining schemas with keys and rules that should be applied to
18
+ values. It supports coercion, input sanitization, custom types and localized error messages
19
+ (with or without I18n gem). It's also used as the schema engine in dry-validation.
20
+ TEXT
21
+ spec.homepage = 'https://dry-rb.org/gems/dry-schema'
22
+ spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-schema.gemspec", "lib/**/*", "config/*.yml"]
23
+ spec.bindir = 'bin'
24
+ spec.executables = []
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
28
+ spec.metadata['changelog_uri'] = 'https://github.com/dry-rb/dry-schema/blob/master/CHANGELOG.md'
29
+ spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-schema'
30
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/dry-rb/dry-schema/issues'
31
+
32
+ spec.required_ruby_version = ">= 2.4.0"
33
+
34
+ # to update dependencies edit project.yml
35
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
36
+ spec.add_runtime_dependency "dry-configurable", "~> 0.8", ">= 0.8.3"
37
+ spec.add_runtime_dependency "dry-core", "~> 0.4"
38
+ spec.add_runtime_dependency "dry-equalizer", "~> 0.2"
39
+ spec.add_runtime_dependency "dry-initializer", "~> 3.0"
40
+ spec.add_runtime_dependency "dry-logic", "~> 1.0"
41
+ spec.add_runtime_dependency "dry-types", "~> 1.4"
42
+
43
+ spec.add_development_dependency "bundler"
44
+ spec.add_development_dependency "rake"
45
+ spec.add_development_dependency "rspec"
46
+ end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema'
3
+ require "dry/schema"
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/extensions'
3
+ require "dry/core/extensions"
4
4
 
5
- require 'dry/schema/constants'
6
- require 'dry/schema/dsl'
7
- require 'dry/schema/params'
8
- require 'dry/schema/json'
5
+ require "dry/schema/config"
6
+ require "dry/schema/constants"
7
+ require "dry/schema/dsl"
8
+ require "dry/schema/params"
9
+ require "dry/schema/json"
9
10
 
10
11
  module Dry
11
12
  # Main interface
@@ -14,6 +15,18 @@ module Dry
14
15
  module Schema
15
16
  extend Dry::Core::Extensions
16
17
 
18
+ # Configuration
19
+ #
20
+ # @example
21
+ # Dry::Schema.config.messages.backend = :i18n
22
+ #
23
+ # @return [Config]
24
+ #
25
+ # @api public
26
+ def self.config
27
+ @config ||= Config.new
28
+ end
29
+
17
30
  # Define a schema
18
31
  #
19
32
  # @example
@@ -30,7 +43,7 @@ module Dry
30
43
  #
31
44
  # @api public
32
45
  def self.define(**options, &block)
33
- DSL.new(options, &block).call
46
+ DSL.new(**options, &block).call
34
47
  end
35
48
 
36
49
  # Define a schema suitable for HTTP params
@@ -74,4 +87,4 @@ module Dry
74
87
  end
75
88
  end
76
89
 
77
- require 'dry/schema/extensions'
90
+ require "dry/schema/extensions"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/logic/rule_compiler'
4
- require 'dry/schema/namespaced_rule'
5
- require 'dry/schema/predicate_registry'
3
+ require "dry/logic/rule_compiler"
4
+ require "dry/schema/namespaced_rule"
5
+ require "dry/schema/predicate_registry"
6
6
 
7
7
  module Dry
8
8
  module Schema
@@ -35,7 +35,7 @@ module Dry
35
35
  # @return [NamespacedRule]
36
36
  #
37
37
  # @api private
38
- def visit_namespace(node, opts = EMPTY_HASH)
38
+ def visit_namespace(node, _opts = EMPTY_HASH)
39
39
  namespace, rest = node
40
40
  NamespacedRule.new(namespace, visit(rest))
41
41
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
4
- require 'dry/configurable'
3
+ require "dry/equalizer"
4
+ require "dry/configurable"
5
5
 
6
- require 'dry/schema/constants'
7
- require 'dry/schema/predicate_registry'
8
- require 'dry/schema/type_container'
6
+ require "dry/schema/constants"
7
+ require "dry/schema/predicate_registry"
8
+ require "dry/schema/type_container"
9
9
 
10
10
  module Dry
11
11
  module Schema
@@ -51,6 +51,15 @@ module Dry
51
51
  setting(:default_locale, nil)
52
52
  end
53
53
 
54
+ # @!method validate_keys
55
+ #
56
+ # On/off switch for key validator
57
+ #
58
+ # @return [Boolean]
59
+ #
60
+ # @api public
61
+ setting(:validate_keys, false)
62
+
54
63
  # @api private
55
64
  def respond_to_missing?(meth, include_private = false)
56
65
  super || config.respond_to?(meth, include_private)
@@ -58,7 +67,7 @@ module Dry
58
67
 
59
68
  # @api private
60
69
  def inspect
61
- "#<#{self.class} #{to_h.map { |k,v| ["#{k}=", v.inspect] }.map(&:join).join(' ')}>"
70
+ "#<#{self.class} #{to_h.map { |k, v| ["#{k}=", v.inspect] }.map(&:join).join(" ")}>"
62
71
  end
63
72
 
64
73
  private
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pathname'
4
- require 'dry/core/constants'
3
+ require "pathname"
4
+ require "dry/core/constants"
5
5
 
6
6
  module Dry
7
7
  # Common constants used across the library
@@ -10,15 +10,24 @@ module Dry
10
10
  module Schema
11
11
  include Core::Constants
12
12
 
13
- LIST_SEPARATOR = ', '
14
- QUESTION_MARK = '?'
15
- DOT = '.'
13
+ LIST_SEPARATOR = ", "
14
+ QUESTION_MARK = "?"
15
+ DOT = "."
16
+
17
+ # core processor steps in the default execution order
18
+ STEPS_IN_ORDER = %i[
19
+ key_validator
20
+ key_coercer
21
+ filter_schema
22
+ value_coercer
23
+ rule_applier
24
+ ].freeze
16
25
 
17
26
  # Path to the default set of localized messages bundled within the gem
18
- DEFAULT_MESSAGES_PATH = Pathname(__dir__).join('../../../config/errors.yml').realpath.freeze
27
+ DEFAULT_MESSAGES_PATH = Pathname(__dir__).join("../../../config/errors.yml").realpath.freeze
19
28
 
20
29
  # Default namespace used for localized messages in YAML files
21
- DEFAULT_MESSAGES_ROOT = 'dry_schema'
30
+ DEFAULT_MESSAGES_ROOT = "dry_schema"
22
31
 
23
32
  # An error raised when DSL is used in an incorrect way
24
33
  InvalidSchemaError = Class.new(StandardError)
@@ -26,10 +35,11 @@ module Dry
26
35
  # An error raised when a localized message cannot be found
27
36
  MissingMessageError = Class.new(StandardError) do
28
37
  # @api private
29
- def initialize(path)
38
+ def initialize(path, paths = [])
30
39
  *rest, rule = path
31
40
  super(<<~STR)
32
- Message template for #{rule.inspect} under #{rest.join(DOT).inspect} was not found
41
+ Message template for #{rule.inspect} under #{rest.join(DOT).inspect} was not found. Searched in:
42
+ #{paths.map { |string| "\"#{string}\"" }.join("\n")}
33
43
  STR
34
44
  end
35
45
  end
@@ -1,18 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/initializer'
4
-
5
- require 'dry/schema/constants'
6
- require 'dry/schema/config'
7
- require 'dry/schema/compiler'
8
- require 'dry/schema/types'
9
- require 'dry/schema/macros'
10
-
11
- require 'dry/schema/processor'
12
- require 'dry/schema/key_map'
13
- require 'dry/schema/key_coercer'
14
- require 'dry/schema/value_coercer'
15
- require 'dry/schema/rule_applier'
3
+ require "dry/initializer"
4
+
5
+ require "dry/schema"
6
+ require "dry/schema/constants"
7
+ require "dry/schema/path"
8
+ require "dry/schema/config"
9
+ require "dry/schema/compiler"
10
+ require "dry/schema/types"
11
+ require "dry/schema/macros"
12
+
13
+ require "dry/schema/processor"
14
+ require "dry/schema/processor_steps"
15
+ require "dry/schema/key_map"
16
+ require "dry/schema/key_coercer"
17
+ require "dry/schema/key_validator"
18
+ require "dry/schema/value_coercer"
19
+ require "dry/schema/rule_applier"
16
20
 
17
21
  module Dry
18
22
  module Schema
@@ -50,8 +54,6 @@ module Dry
50
54
 
51
55
  extend Dry::Initializer
52
56
 
53
- include ::Dry::Equalizer(:options)
54
-
55
57
  # @return [Compiler] The rule compiler object
56
58
  option :compiler, default: -> { Compiler.new }
57
59
 
@@ -64,18 +66,24 @@ module Dry
64
66
  # @return [Compiler] A key=>type map defined within the DSL
65
67
  option :types, default: -> { EMPTY_HASH.dup }
66
68
 
67
- # @return [DSL] An optional parent DSL object that will be used to merge keys and rules
68
- option :parent, optional: true
69
+ # @return [Array] Optional parent DSL objects, that will be used to merge keys and rules
70
+ option :parent, Types::Coercible::Array, default: -> { EMPTY_ARRAY.dup }, as: :parents
69
71
 
70
72
  # @return [Config] Configuration object exposed via `#configure` method
71
- option :config, optional: true, default: proc { parent ? parent.config.dup : Config.new }
73
+ option :config, optional: true, default: proc { default_config }
74
+
75
+ # @return [ProcessorSteps] Steps for the processor
76
+ option :steps, default: proc { ProcessorSteps.new }
77
+
78
+ # @return [Path, Array] Path under which the schema is defined
79
+ option :path, -> *args { Path[*args] if args.any? }, default: proc { EMPTY_ARRAY }
72
80
 
73
81
  # Build a new DSL object and evaluate provided block
74
82
  #
75
83
  # @param [Hash] options
76
84
  # @option options [Class] :processor The processor type (`Params`, `JSON` or a custom sub-class)
77
85
  # @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)
86
+ # @option options [Array[DSL]] :parent One or more instances of the parent DSL (optional)
79
87
  # @option options [Config] :config A configuration object (optional)
80
88
  #
81
89
  # @see Schema.define
@@ -169,7 +177,7 @@ module Dry
169
177
  def key(name, macro:, &block)
170
178
  raise ArgumentError, "Key +#{name}+ is not a symbol" unless name.is_a?(::Symbol)
171
179
 
172
- set_type(name, Types::Any)
180
+ set_type(name, Types::Any.meta(default: true))
173
181
 
174
182
  macro = macro.new(
175
183
  name: name,
@@ -189,11 +197,31 @@ module Dry
189
197
  #
190
198
  # @api private
191
199
  def call
192
- steps = [key_coercer]
193
- steps << filter_schema.rule_applier if filter_rules?
194
- steps << value_coercer << rule_applier
200
+ all_steps = parents.map(&:steps) + [steps]
201
+
202
+ result_steps = all_steps.inject { |result, steps| result.merge(steps) }
203
+
204
+ result_steps[:key_validator] = key_validator if config.validate_keys
205
+ result_steps[:key_coercer] = key_coercer
206
+ result_steps[:value_coercer] = value_coercer
207
+ result_steps[:rule_applier] = rule_applier
208
+ result_steps[:filter_schema] = filter_schema.rule_applier if filter_rules?
195
209
 
196
- processor_type.new(schema_dsl: self, steps: steps)
210
+ processor_type.new(schema_dsl: self, steps: result_steps)
211
+ end
212
+
213
+ # Merge with another dsl
214
+ #
215
+ # @return [DSL]
216
+ #
217
+ # @api private
218
+ def merge(other)
219
+ new(
220
+ parent: parents + other.parents,
221
+ macros: macros + other.macros,
222
+ types: types.merge(other.types),
223
+ steps: steps.merge(other.steps)
224
+ )
197
225
  end
198
226
 
199
227
  # Cast this DSL into a rule object
@@ -212,7 +240,46 @@ module Dry
212
240
  #
213
241
  # @api public
214
242
  def array
215
- -> member_type { type_registry['array'].of(resolve_type(member_type)) }
243
+ -> member_type { type_registry["array"].of(resolve_type(member_type)) }
244
+ end
245
+
246
+ # Method allows steps injection to the processor
247
+ #
248
+ # @example
249
+ # before(:rule_applier) do |input|
250
+ # input.compact
251
+ # end
252
+ #
253
+ # @return [DSL]
254
+ #
255
+ # @api public
256
+ def before(key, &block)
257
+ steps.before(key, &block)
258
+ self
259
+ end
260
+
261
+ # Method allows steps injection to the processor
262
+ #
263
+ # @example
264
+ # after(:rule_applier) do |input|
265
+ # input.compact
266
+ # end
267
+ #
268
+ # @return [DSL]
269
+ #
270
+ # @api public
271
+ def after(key, &block)
272
+ steps.after(key, &block)
273
+ self
274
+ end
275
+
276
+ # The parent (last from parents) which is used for copying non mergeable configuration
277
+ #
278
+ # @return DSL
279
+ #
280
+ # @api public
281
+ def parent
282
+ @parent ||= parents.last
216
283
  end
217
284
 
218
285
  # Return type schema used by the value coercer
@@ -221,8 +288,9 @@ module Dry
221
288
  #
222
289
  # @api private
223
290
  def type_schema
224
- schema = type_registry['hash'].schema(types).lax
225
- parent ? parent.type_schema.schema(schema.to_a) : schema
291
+ our_schema = type_registry["hash"].schema(types).lax
292
+ schemas = [*parents.map(&:type_schema), our_schema]
293
+ schemas.inject { |result, schema| result.schema(schema.to_a) }
226
294
  end
227
295
 
228
296
  # Return a new DSL instance using the same processor type
@@ -230,8 +298,8 @@ module Dry
230
298
  # @return [Dry::Types::Safe]
231
299
  #
232
300
  # @api private
233
- def new(options = EMPTY_HASH, &block)
234
- self.class.new(options.merge(processor_type: processor_type, config: config), &block)
301
+ def new(**options, &block)
302
+ self.class.new(**options, processor_type: processor_type, config: config, &block)
235
303
  end
236
304
 
237
305
  # Set a type for the given key name
@@ -244,11 +312,20 @@ module Dry
244
312
  # @api private
245
313
  def set_type(name, spec)
246
314
  type = resolve_type(spec)
247
- meta = { required: false, maybe: type.optional? }
315
+ meta = {required: false, maybe: type.optional?}
248
316
 
249
317
  types[name] = type.meta(meta)
250
318
  end
251
319
 
320
+ # Check if a custom type was set under provided key name
321
+ #
322
+ # @return [Bool]
323
+ #
324
+ # @api private
325
+ def custom_type?(name)
326
+ !types[name].meta[:default].equal?(true)
327
+ end
328
+
252
329
  # Resolve type object from the provided spec
253
330
  #
254
331
  # @param [Symbol, Array<Symbol>, Dry::Types::Type] spec
@@ -276,14 +353,18 @@ module Dry
276
353
  #
277
354
  # @api private
278
355
  def filter_schema_dsl
279
- @filter_schema_dsl ||= new(parent: parent_filter_schema)
356
+ @filter_schema_dsl ||= new(parent: parent_filter_schemas)
280
357
  end
281
358
 
282
359
  # Check if any filter rules were defined
283
360
  #
284
361
  # @api private
285
362
  def filter_rules?
286
- (instance_variable_defined?('@filter_schema_dsl') && !filter_schema_dsl.macros.empty?) || parent&.filter_rules?
363
+ if instance_variable_defined?("@filter_schema_dsl") && !filter_schema_dsl.macros.empty?
364
+ return true
365
+ end
366
+
367
+ parents.any?(&:filter_rules?)
287
368
  end
288
369
 
289
370
  protected
@@ -323,10 +404,17 @@ module Dry
323
404
  private
324
405
 
325
406
  # @api private
326
- def parent_filter_schema
327
- return unless parent
407
+ def parent_filter_schemas
408
+ parents.select(&:filter_rules?).map(&:filter_schema)
409
+ end
328
410
 
329
- parent.filter_schema if parent.filter_rules?
411
+ # Build a key validator
412
+ #
413
+ # @return [KeyValidator]
414
+ #
415
+ # @api private
416
+ def key_validator
417
+ KeyValidator.new(key_map: key_map + parent_key_map)
330
418
  end
331
419
 
332
420
  # Build a key coercer
@@ -366,13 +454,19 @@ module Dry
366
454
 
367
455
  # Build a key spec needed by the key map
368
456
  #
457
+ # TODO: we need a key-map compiler using Types AST
458
+ #
369
459
  # @api private
370
460
  def key_spec(name, type)
371
461
  if type.respond_to?(:keys)
372
- { name => key_map(type.name_key_map) }
462
+ {name => key_map(type.name_key_map)}
373
463
  elsif type.respond_to?(:member)
374
464
  kv = key_spec(name, type.member)
375
465
  kv.equal?(name) ? name : kv.flatten(1)
466
+ elsif type.meta[:maybe] && type.respond_to?(:right)
467
+ key_spec(name, type.right)
468
+ elsif type.respond_to?(:type)
469
+ key_spec(name, type.type)
376
470
  else
377
471
  name
378
472
  end
@@ -380,12 +474,24 @@ module Dry
380
474
 
381
475
  # @api private
382
476
  def parent_rules
383
- parent&.rules || EMPTY_HASH
477
+ parents.reduce({}) { |rules, parent| rules.merge(parent.rules) }
384
478
  end
385
479
 
386
480
  # @api private
387
481
  def parent_key_map
388
- parent&.key_map || EMPTY_ARRAY
482
+ parents.reduce([]) { |key_map, parent| parent.key_map + key_map }
483
+ end
484
+
485
+ # @api private
486
+ def default_config
487
+ parents.each_cons(2) do |left, right|
488
+ unless left.config == right.config
489
+ raise ArgumentError,
490
+ "Parent configs differ, left=#{left.inspect}, right=#{right.inspect}"
491
+ end
492
+ end
493
+
494
+ (parent || Schema).config.dup
389
495
  end
390
496
  end
391
497
  end