dry-schema 1.4.3 → 1.5.0

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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +170 -97
  3. data/config/errors.yml +4 -0
  4. data/dry-schema.gemspec +46 -0
  5. data/lib/dry-schema.rb +1 -1
  6. data/lib/dry/schema.rb +19 -6
  7. data/lib/dry/schema/compiler.rb +4 -4
  8. data/lib/dry/schema/config.rb +15 -6
  9. data/lib/dry/schema/constants.rb +16 -7
  10. data/lib/dry/schema/dsl.rb +88 -27
  11. data/lib/dry/schema/extensions.rb +10 -2
  12. data/lib/dry/schema/extensions/hints.rb +15 -8
  13. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +1 -1
  14. data/lib/dry/schema/extensions/hints/message_set_methods.rb +0 -47
  15. data/lib/dry/schema/extensions/info.rb +27 -0
  16. data/lib/dry/schema/extensions/info/schema_compiler.rb +105 -0
  17. data/lib/dry/schema/extensions/monads.rb +1 -1
  18. data/lib/dry/schema/extensions/struct.rb +32 -0
  19. data/lib/dry/schema/json.rb +1 -1
  20. data/lib/dry/schema/key.rb +16 -1
  21. data/lib/dry/schema/key_coercer.rb +4 -4
  22. data/lib/dry/schema/key_map.rb +9 -4
  23. data/lib/dry/schema/key_validator.rb +66 -0
  24. data/lib/dry/schema/macros.rb +8 -8
  25. data/lib/dry/schema/macros/array.rb +17 -4
  26. data/lib/dry/schema/macros/core.rb +9 -4
  27. data/lib/dry/schema/macros/dsl.rb +34 -19
  28. data/lib/dry/schema/macros/each.rb +4 -4
  29. data/lib/dry/schema/macros/filled.rb +5 -5
  30. data/lib/dry/schema/macros/hash.rb +21 -3
  31. data/lib/dry/schema/macros/key.rb +9 -9
  32. data/lib/dry/schema/macros/maybe.rb +3 -3
  33. data/lib/dry/schema/macros/optional.rb +1 -1
  34. data/lib/dry/schema/macros/required.rb +1 -1
  35. data/lib/dry/schema/macros/schema.rb +23 -2
  36. data/lib/dry/schema/macros/value.rb +32 -10
  37. data/lib/dry/schema/message.rb +35 -9
  38. data/lib/dry/schema/message/or.rb +18 -39
  39. data/lib/dry/schema/message/or/abstract.rb +28 -0
  40. data/lib/dry/schema/message/or/multi_path.rb +37 -0
  41. data/lib/dry/schema/message/or/single_path.rb +64 -0
  42. data/lib/dry/schema/message_compiler.rb +37 -17
  43. data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
  44. data/lib/dry/schema/message_set.rb +25 -36
  45. data/lib/dry/schema/messages.rb +6 -6
  46. data/lib/dry/schema/messages/abstract.rb +54 -56
  47. data/lib/dry/schema/messages/i18n.rb +29 -27
  48. data/lib/dry/schema/messages/namespaced.rb +12 -2
  49. data/lib/dry/schema/messages/template.rb +19 -44
  50. data/lib/dry/schema/messages/yaml.rb +60 -13
  51. data/lib/dry/schema/params.rb +1 -1
  52. data/lib/dry/schema/path.rb +44 -5
  53. data/lib/dry/schema/predicate.rb +2 -2
  54. data/lib/dry/schema/predicate_inferrer.rb +4 -184
  55. data/lib/dry/schema/predicate_registry.rb +2 -2
  56. data/lib/dry/schema/primitive_inferrer.rb +16 -0
  57. data/lib/dry/schema/processor.rb +49 -28
  58. data/lib/dry/schema/processor_steps.rb +50 -27
  59. data/lib/dry/schema/result.rb +43 -5
  60. data/lib/dry/schema/rule_applier.rb +8 -7
  61. data/lib/dry/schema/step.rb +79 -0
  62. data/lib/dry/schema/trace.rb +5 -4
  63. data/lib/dry/schema/type_container.rb +3 -3
  64. data/lib/dry/schema/type_registry.rb +2 -2
  65. data/lib/dry/schema/types.rb +1 -1
  66. data/lib/dry/schema/value_coercer.rb +2 -2
  67. data/lib/dry/schema/version.rb +1 -1
  68. metadata +22 -8
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/initializer'
3
+ require "dry/initializer"
4
4
 
5
- require 'dry/schema/constants'
6
- require 'dry/schema/compiler'
7
- require 'dry/schema/trace'
5
+ require "dry/schema/constants"
6
+ require "dry/schema/compiler"
7
+ require "dry/schema/trace"
8
8
 
9
9
  module Dry
10
10
  module Schema
@@ -32,6 +32,11 @@ module Dry
32
32
  self.class.new(name: name, compiler: compiler, schema_dsl: schema_dsl, **options)
33
33
  end
34
34
 
35
+ # @api private
36
+ def path
37
+ schema_dsl.path
38
+ end
39
+
35
40
  # @api private
36
41
  def to_rule
37
42
  compiler.visit(to_ast)
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/logic/operators'
4
- require 'dry/types/predicate_inferrer'
5
- require 'dry/types/primitive_inferrer'
3
+ require "dry/logic/operators"
6
4
 
7
- require 'dry/schema/macros/core'
5
+ require "dry/schema/macros/core"
6
+ require "dry/schema/predicate_inferrer"
7
+ require "dry/schema/primitive_inferrer"
8
8
 
9
9
  module Dry
10
10
  module Schema
@@ -28,13 +28,13 @@ module Dry
28
28
  # PredicateInferrer is used to infer predicate type-check from a type spec
29
29
  # @return [PredicateInferrer]
30
30
  # @api private
31
- option :predicate_inferrer, default: proc { ::Dry::Types::PredicateInferrer.new(compiler.predicates) }
31
+ option :predicate_inferrer, default: proc { PredicateInferrer.new(compiler.predicates) }
32
32
 
33
33
  # @!attribute [r] primitive_inferrer
34
34
  # PrimitiveInferrer used to get a list of primitive classes from configured type
35
35
  # @return [PrimitiveInferrer]
36
36
  # @api private
37
- option :primitive_inferrer, default: proc { ::Dry::Types::PrimitiveInferrer.new }
37
+ option :primitive_inferrer, default: proc { PrimitiveInferrer.new }
38
38
 
39
39
  # @overload value(*predicates, **predicate_opts)
40
40
  # Set predicates without and with arguments
@@ -177,6 +177,11 @@ module Dry
177
177
  self
178
178
  end
179
179
 
180
+ # @api private
181
+ def custom_type?
182
+ schema_dsl.custom_type?(name)
183
+ end
184
+
180
185
  private
181
186
 
182
187
  # @api private
@@ -195,29 +200,32 @@ module Dry
195
200
 
196
201
  # @api private
197
202
  def extract_type_spec(*args, nullable: false, set_type: true)
198
- type_spec = args[0]
199
-
200
- is_type_spec = type_spec.is_a?(Dry::Schema::Processor) ||
201
- type_spec.is_a?(Symbol) &&
202
- type_spec.to_s.end_with?(QUESTION_MARK)
203
-
204
- type_spec = nil if is_type_spec
203
+ type_spec = args[0] unless schema_or_predicate?(args[0])
205
204
 
206
205
  predicates = Array(type_spec ? args[1..-1] : args)
206
+ type_rule = nil
207
207
 
208
208
  if type_spec
209
209
  resolved_type = resolve_type(type_spec, nullable)
210
210
 
211
- type(resolved_type) if set_type
212
-
213
- type_predicates = predicate_inferrer[resolved_type]
211
+ if type_spec.is_a?(::Array)
212
+ type_rule = type_spec.map { |ts| new(chain: false).value(ts) }.reduce(:|)
213
+ else
214
+ type_predicates = predicate_inferrer[resolved_type]
214
215
 
215
- predicates.replace(type_predicates + predicates) unless type_predicates.empty?
216
+ predicates.replace(type_predicates + predicates) unless type_predicates.empty?
216
217
 
217
- return self if predicates.empty?
218
+ return self if predicates.empty?
219
+ end
218
220
  end
219
221
 
220
- yield(*predicates, type_spec: type_spec)
222
+ type(resolved_type) if set_type && resolved_type
223
+
224
+ if type_rule
225
+ yield(*predicates, type_spec: nil, type_rule: type_rule)
226
+ else
227
+ yield(*predicates, type_spec: type_spec, type_rule: nil)
228
+ end
221
229
  end
222
230
 
223
231
  # @api private
@@ -230,6 +238,13 @@ module Dry
230
238
  schema_dsl.resolve_type([:nil, resolved])
231
239
  end
232
240
  end
241
+
242
+ # @api private
243
+ def schema_or_predicate?(arg)
244
+ arg.is_a?(Dry::Schema::Processor) ||
245
+ arg.is_a?(Symbol) &&
246
+ arg.to_s.end_with?(QUESTION_MARK)
247
+ end
233
248
  end
234
249
  end
235
250
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/types/type'
4
- require 'dry/schema/macros/dsl'
3
+ require "dry/types/type"
4
+ require "dry/schema/macros/dsl"
5
5
 
6
6
  module Dry
7
7
  module Schema
@@ -12,12 +12,12 @@ module Dry
12
12
  class Each < DSL
13
13
  # @api private
14
14
  def value(*args, **opts)
15
- extract_type_spec(*args, set_type: false) do |*predicates, type_spec:|
15
+ extract_type_spec(*args, set_type: false) do |*predicates, type_spec:, type_rule:|
16
16
  if type_spec && !type_spec.is_a?(Dry::Types::Type)
17
17
  type(schema_dsl.array[type_spec])
18
18
  end
19
19
 
20
- super(*predicates, type_spec: type_spec, **opts)
20
+ super(*predicates, type_spec: type_spec, type_rule: type_rule, **opts)
21
21
  end
22
22
  end
23
23
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/macros/value'
3
+ require "dry/schema/macros/value"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -15,23 +15,23 @@ module Dry
15
15
 
16
16
  if opts[:type_spec] && !filter_empty_string?
17
17
  value(predicates[0], :filled?, *predicates[1..predicates.size - 1], **opts, &block)
18
+ elsif opts[:type_rule]
19
+ value(:filled?).value(*predicates, **opts, &block)
18
20
  else
19
21
  value(:filled?, *predicates, **opts, &block)
20
22
  end
21
23
  end
22
24
 
23
25
  # @api private
24
- # rubocop:disable Style/GuardClause
25
26
  def ensure_valid_predicates(predicates)
26
27
  if predicates.include?(:empty?)
27
- raise ::Dry::Schema::InvalidSchemaError, 'Using filled with empty? predicate is invalid'
28
+ raise ::Dry::Schema::InvalidSchemaError, "Using filled with empty? predicate is invalid"
28
29
  end
29
30
 
30
31
  if predicates.include?(:filled?)
31
- raise ::Dry::Schema::InvalidSchemaError, 'Using filled with filled? is redundant'
32
+ raise ::Dry::Schema::InvalidSchemaError, "Using filled with filled? is redundant"
32
33
  end
33
34
  end
34
- # rubocop:enable Style/GuardClause
35
35
 
36
36
  # @api private
37
37
  def filter_empty_string?
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/macros/schema'
3
+ require "dry/schema/macros/schema"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -11,8 +11,26 @@ module Dry
11
11
  class Hash < Schema
12
12
  # @api private
13
13
  def call(*args, &block)
14
- trace << hash?
15
- super(*args, &block)
14
+ if args.size >= 1 && args[0].respond_to?(:keys)
15
+ hash_type = args[0]
16
+ type_predicates = predicate_inferrer[hash_type]
17
+ all_predicats = type_predicates + args.drop(1)
18
+
19
+ super(*all_predicats) do
20
+ hash_type.each do |key|
21
+ if key.required?
22
+ required(key.name).value(key.type)
23
+ else
24
+ optional(key.name).value(key.type)
25
+ end
26
+ instance_exec(&block) if block
27
+ end
28
+ end
29
+ else
30
+ trace << hash?
31
+
32
+ super(*args, &block)
33
+ end
16
34
  end
17
35
  end
18
36
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/processor'
4
- require 'dry/schema/macros/dsl'
5
- require 'dry/schema/constants'
3
+ require "dry/schema/processor"
4
+ require "dry/schema/macros/dsl"
5
+ require "dry/schema/constants"
6
6
 
7
7
  module Dry
8
8
  module Schema
@@ -54,8 +54,8 @@ module Dry
54
54
  #
55
55
  # @api public
56
56
  def value(*args, **opts, &block)
57
- extract_type_spec(*args) do |*predicates, type_spec:|
58
- super(*predicates, type_spec: type_spec, **opts, &block)
57
+ extract_type_spec(*args) do |*predicates, type_spec:, type_rule:|
58
+ super(*predicates, type_spec: type_spec, type_rule: type_rule, **opts, &block)
59
59
  end
60
60
  end
61
61
 
@@ -70,8 +70,8 @@ module Dry
70
70
  #
71
71
  # @api public
72
72
  def filled(*args, **opts, &block)
73
- extract_type_spec(*args) do |*predicates, type_spec:|
74
- super(*predicates, type_spec: type_spec, **opts, &block)
73
+ extract_type_spec(*args) do |*predicates, type_spec:, type_rule:|
74
+ super(*predicates, type_spec: type_spec, type_rule: type_rule, **opts, &block)
75
75
  end
76
76
  end
77
77
 
@@ -86,9 +86,9 @@ module Dry
86
86
  #
87
87
  # @api public
88
88
  def maybe(*args, **opts, &block)
89
- extract_type_spec(*args, nullable: true) do |*predicates, type_spec:|
89
+ extract_type_spec(*args, nullable: true) do |*predicates, type_spec:, type_rule:|
90
90
  append_macro(Macros::Maybe) do |macro|
91
- macro.call(*predicates, type_spec: type_spec, **opts, &block)
91
+ macro.call(*predicates, type_spec: type_spec, type_rule: type_rule, **opts, &block)
92
92
  end
93
93
  end
94
94
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/macros/dsl'
3
+ require "dry/schema/macros/dsl"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -12,11 +12,11 @@ module Dry
12
12
  # @api private
13
13
  def call(*args, **opts, &block)
14
14
  if args.include?(:empty?)
15
- raise ::Dry::Schema::InvalidSchemaError, 'Using maybe with empty? predicate is invalid'
15
+ raise ::Dry::Schema::InvalidSchemaError, "Using maybe with empty? predicate is invalid"
16
16
  end
17
17
 
18
18
  if args.include?(:nil?)
19
- raise ::Dry::Schema::InvalidSchemaError, 'Using maybe with nil? predicate is redundant'
19
+ raise ::Dry::Schema::InvalidSchemaError, "Using maybe with nil? predicate is redundant"
20
20
  end
21
21
 
22
22
  value(*args, **opts, &block)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/macros/key'
3
+ require "dry/schema/macros/key"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/macros/key'
3
+ require "dry/schema/macros/key"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/macros/value'
3
+ require "dry/schema/macros/value"
4
4
 
5
5
  module Dry
6
6
  module Schema
@@ -13,8 +13,13 @@ module Dry
13
13
  def call(*args, &block)
14
14
  super(*args, &nil) unless args.empty?
15
15
 
16
+ if args.size.equal?(1) && (op = args.first).is_a?(Dry::Logic::Operations::Abstract)
17
+ process_operation(op)
18
+ end
19
+
16
20
  if block
17
21
  schema = define(*args, &block)
22
+ import_steps(schema)
18
23
  trace << schema.to_rule
19
24
  end
20
25
 
@@ -23,9 +28,25 @@ module Dry
23
28
 
24
29
  private
25
30
 
31
+ # @api private
32
+ def process_operation(op)
33
+ schemas = op.rules.select { |rule| rule.is_a?(Processor) }
34
+
35
+ hash_schema = hash_type.schema(
36
+ schemas.map(&:schema_dsl).map(&:types).reduce(:merge)
37
+ )
38
+
39
+ type(hash_schema)
40
+ end
41
+
42
+ # @api private
43
+ def hash_type
44
+ schema_dsl.resolve_type(:hash)
45
+ end
46
+
26
47
  # @api private
27
48
  def define(*args, &block)
28
- definition = schema_dsl.new(&block)
49
+ definition = schema_dsl.new(path: schema_dsl.path, &block)
29
50
  schema = definition.call
30
51
  type_schema =
31
52
  if array_type?(parent_type)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/macros/dsl'
3
+ require "dry/schema/path"
4
+ require "dry/schema/macros/dsl"
4
5
 
5
6
  module Dry
6
7
  module Schema
@@ -13,6 +14,8 @@ module Dry
13
14
  def call(*predicates, **opts, &block)
14
15
  schema = predicates.detect { |predicate| predicate.is_a?(Processor) }
15
16
 
17
+ type_spec = opts[:type_spec]
18
+
16
19
  if schema
17
20
  current_type = schema_dsl.types[name]
18
21
 
@@ -23,20 +26,30 @@ module Dry
23
26
  schema.type_schema
24
27
  end
25
28
 
26
- schema_dsl.set_type(name, updated_type)
29
+ import_steps(schema)
30
+
31
+ type(updated_type) unless custom_type? && !current_type.respond_to?(:of)
27
32
  end
28
33
 
29
- trace.evaluate(*predicates, **opts)
34
+ trace_opts = opts.reject { |key, _| key == :type_spec || key == :type_rule }
30
35
 
31
- type_spec = opts[:type_spec]
32
- if block && type_spec.equal?(:hash)
33
- hash(&block)
34
- elsif block
35
- trace.append(new(chain: false).instance_exec(&block))
36
+ if (type_rule = opts[:type_rule])
37
+ trace.append(type_rule).evaluate(*predicates, **trace_opts)
38
+ trace.append(new(chain: false).instance_exec(&block)) if block
39
+ else
40
+ trace.evaluate(*predicates, **trace_opts)
41
+
42
+ if block && type_spec.equal?(:hash)
43
+ hash(&block)
44
+ elsif type_spec.is_a?(::Dry::Types::Type) && hash_type?(type_spec)
45
+ hash(type_spec)
46
+ elsif block
47
+ trace.append(new(chain: false).instance_exec(&block))
48
+ end
36
49
  end
37
50
 
38
51
  if trace.captures.empty?
39
- raise ArgumentError, 'wrong number of arguments (given 0, expected at least 1)'
52
+ raise ArgumentError, "wrong number of arguments (given 0, expected at least 1)"
40
53
  end
41
54
 
42
55
  each(type_spec.type.member) if type_spec.respond_to?(:member)
@@ -49,12 +62,16 @@ module Dry
49
62
  primitive_inferrer[type].eql?([::Array])
50
63
  end
51
64
 
65
+ def hash_type?(type)
66
+ primitive_inferrer[type].eql?([::Hash])
67
+ end
68
+
52
69
  # @api private
53
70
  def build_array_type(array_type, member)
54
71
  if array_type.respond_to?(:of)
55
72
  array_type.of(member)
56
73
  else
57
- raise ArgumentError, <<~ERROR.split("\n").join(' ')
74
+ raise ArgumentError, <<~ERROR.split("\n").join(" ")
58
75
  Cannot define schema for a nominal array type.
59
76
  Array types must be instances of Dry::Types::Array,
60
77
  usually constructed with Types::Constructor(Array) { ... } or
@@ -63,6 +80,11 @@ module Dry
63
80
  end
64
81
  end
65
82
 
83
+ # @api private
84
+ def import_steps(schema)
85
+ schema_dsl.steps.import_callbacks(Path[[*path, name]], schema.steps)
86
+ end
87
+
66
88
  # @api private
67
89
  def respond_to_missing?(meth, include_private = false)
68
90
  super || meth.to_s.end_with?(QUESTION_MARK)
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/initializer'
4
- require 'dry/equalizer'
3
+ require "dry/initializer"
4
+ require "dry/equalizer"
5
5
 
6
- require 'dry/schema/path'
7
- require 'dry/schema/message/or'
6
+ require "dry/schema/path"
7
+ require "dry/schema/message/or"
8
8
 
9
9
  module Dry
10
10
  module Schema
@@ -52,9 +52,22 @@ module Dry
52
52
  #
53
53
  # @api public
54
54
  def dump
55
- @dump ||= meta.empty? ? text : { text: text, **meta }
55
+ @dump ||= meta.empty? ? text : {text: text, **meta}
56
+ end
57
+ alias_method :to_s, :dump
58
+
59
+ # Dump the message into a hash
60
+ #
61
+ # The hash will be deeply nested if the path's size is greater than 1
62
+ #
63
+ # @see Message#to_h
64
+ #
65
+ # @return [Hash]
66
+ #
67
+ # @api public
68
+ def to_h
69
+ @to_h ||= _path.to_h(dump)
56
70
  end
57
- alias to_s dump
58
71
 
59
72
  # See if another message is the same
60
73
  #
@@ -69,19 +82,32 @@ module Dry
69
82
  other.is_a?(String) ? text == other : super
70
83
  end
71
84
 
85
+ # @api private
86
+ def to_or(root)
87
+ clone = dup
88
+ clone.instance_variable_set("@path", path - root.to_a)
89
+ clone.instance_variable_set("@_path", nil)
90
+ clone
91
+ end
92
+
72
93
  # See which message is higher in the hierarchy
73
94
  #
74
95
  # @api private
75
96
  def <=>(other)
76
- l_path = Path[path]
77
- r_path = Path[other.path]
97
+ l_path = _path
98
+ r_path = other._path
78
99
 
79
100
  unless l_path.same_root?(r_path)
80
- raise ArgumentError, 'Cannot compare messages from different root paths'
101
+ raise ArgumentError, "Cannot compare messages from different root paths"
81
102
  end
82
103
 
83
104
  l_path <=> r_path
84
105
  end
106
+
107
+ # @api private
108
+ def _path
109
+ @_path ||= Path[path]
110
+ end
85
111
  end
86
112
  end
87
113
  end