dry-schema 1.4.1 → 1.5.2

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +210 -73
  3. data/LICENSE +1 -1
  4. data/README.md +4 -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 +5 -5
  10. data/lib/dry/schema/config.rb +15 -6
  11. data/lib/dry/schema/constants.rb +16 -7
  12. data/lib/dry/schema/dsl.rb +89 -31
  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 +67 -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 +44 -23
  30. data/lib/dry/schema/macros/each.rb +4 -4
  31. data/lib/dry/schema/macros/filled.rb +5 -5
  32. data/lib/dry/schema/macros/hash.rb +21 -3
  33. data/lib/dry/schema/macros/key.rb +10 -9
  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 +58 -22
  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 +54 -62
  49. data/lib/dry/schema/messages/i18n.rb +36 -10
  50. data/lib/dry/schema/messages/namespaced.rb +12 -2
  51. data/lib/dry/schema/messages/template.rb +19 -44
  52. data/lib/dry/schema/messages/yaml.rb +61 -14
  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 +4 -2
  56. data/lib/dry/schema/predicate_inferrer.rb +4 -184
  57. data/lib/dry/schema/predicate_registry.rb +2 -2
  58. data/lib/dry/schema/primitive_inferrer.rb +16 -0
  59. data/lib/dry/schema/processor.rb +50 -29
  60. data/lib/dry/schema/processor_steps.rb +50 -27
  61. data/lib/dry/schema/result.rb +53 -6
  62. data/lib/dry/schema/rule_applier.rb +7 -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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/logic/predicates'
4
- require 'dry/types/predicate_registry'
3
+ require "dry/logic/predicates"
4
+ require "dry/types/predicate_registry"
5
5
 
6
6
  module Dry
7
7
  module Schema
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types/primitive_inferrer"
4
+
5
+ module Dry
6
+ module Schema
7
+ # @api private
8
+ class PrimitiveInferrer < ::Dry::Types::PrimitiveInferrer
9
+ Compiler = ::Class.new(superclass::Compiler)
10
+
11
+ def initialize
12
+ @compiler = Compiler.new
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/configurable'
4
- require 'dry/initializer'
3
+ require "dry/configurable"
4
+ require "dry/initializer"
5
+ require "dry/logic/operators"
5
6
 
6
- require 'dry/schema/type_registry'
7
- require 'dry/schema/type_container'
8
- require 'dry/schema/processor_steps'
9
- require 'dry/schema/rule_applier'
10
- require 'dry/schema/key_coercer'
11
- require 'dry/schema/value_coercer'
7
+ require "dry/schema/type_registry"
8
+ require "dry/schema/type_container"
9
+ require "dry/schema/processor_steps"
10
+ require "dry/schema/rule_applier"
11
+ require "dry/schema/key_coercer"
12
+ require "dry/schema/value_coercer"
12
13
 
13
14
  module Dry
14
15
  module Schema
@@ -24,6 +25,8 @@ module Dry
24
25
  extend Dry::Initializer
25
26
  extend Dry::Configurable
26
27
 
28
+ include Dry::Logic::Operators
29
+
27
30
  setting :key_map_type
28
31
  setting :type_registry_namespace, :strict
29
32
  setting :filter_empty_string, false
@@ -62,13 +65,13 @@ module Dry
62
65
  # @api public
63
66
  def new(options = nil, &block)
64
67
  if options || block
65
- processor = super
68
+ processor = super(**(options || EMPTY_HASH))
66
69
  yield(processor) if block
67
70
  processor
68
71
  elsif definition
69
72
  definition.call
70
73
  else
71
- raise ArgumentError, 'Cannot create a schema without a definition'
74
+ raise ArgumentError, "Cannot create a schema without a definition"
72
75
  end
73
76
  end
74
77
  end
@@ -81,28 +84,36 @@ module Dry
81
84
  #
82
85
  # @api public
83
86
  def call(input)
84
- Result.new(input, message_compiler: message_compiler) do |result|
87
+ Result.new(input, input: input, message_compiler: message_compiler) do |result|
85
88
  steps.call(result)
86
89
  end
87
90
  end
88
91
  alias_method :[], :call
89
92
 
90
- # Return a proc that acts like a schema object
93
+ # @api public
94
+ def xor(other)
95
+ raise NotImplementedError, "composing schemas using `xor` operator is not supported yet"
96
+ end
97
+ alias_method :^, :xor
98
+
99
+ # Merge with another schema
91
100
  #
92
- # @return [Proc]
101
+ # @param [Processor] other
102
+ #
103
+ # @return [Processor, Params, JSON]
93
104
  #
94
105
  # @api public
95
- def to_proc
96
- ->(input) { call(input) }
106
+ def merge(other)
107
+ schema_dsl.merge(other.schema_dsl).()
97
108
  end
98
109
 
99
- # Return the key map
110
+ # Return a proc that acts like a schema object
100
111
  #
101
- # @return [KeyMap]
112
+ # @return [Proc]
102
113
  #
103
114
  # @api public
104
- def key_map
105
- steps[:key_coercer].key_map
115
+ def to_proc
116
+ ->(input) { call(input) }
106
117
  end
107
118
 
108
119
  # Return string represntation
@@ -116,15 +127,32 @@ module Dry
116
127
  STR
117
128
  end
118
129
 
130
+ # Return the key map
131
+ #
132
+ # @return [KeyMap]
133
+ #
134
+ # @api public
135
+ def key_map
136
+ steps.key_map
137
+ end
138
+
119
139
  # Return the type schema
120
140
  #
121
141
  # @return [Dry::Types::Safe]
122
142
  #
123
143
  # @api private
124
144
  def type_schema
125
- steps[:value_coercer].type_schema
145
+ steps.type_schema
126
146
  end
127
147
 
148
+ # Return the rule applier
149
+ #
150
+ # @api private
151
+ def rule_applier
152
+ steps.rule_applier
153
+ end
154
+ alias_method :to_rule, :rule_applier
155
+
128
156
  # Return the rules config
129
157
  #
130
158
  # @return [Dry::Types::Config]
@@ -137,9 +165,10 @@ module Dry
137
165
  # Return AST representation of the rules
138
166
  #
139
167
  # @api private
140
- def to_ast
168
+ def to_ast(*)
141
169
  rule_applier.to_ast
142
170
  end
171
+ alias_method :ast, :to_ast
143
172
 
144
173
  # Return the message compiler
145
174
  #
@@ -159,14 +188,6 @@ module Dry
159
188
  rule_applier.rules
160
189
  end
161
190
 
162
- # Return the rule applier
163
- #
164
- # @api private
165
- def rule_applier
166
- steps[:rule_applier]
167
- end
168
- alias_method :to_rule, :rule_applier
169
-
170
191
  # Check if there are filter rules
171
192
  #
172
193
  # @api private
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/initializer'
3
+ require "dry/initializer"
4
+
5
+ require "dry/schema/constants"
6
+ require "dry/schema/step"
4
7
 
5
8
  module Dry
6
9
  module Schema
@@ -20,8 +23,6 @@ module Dry
20
23
  class ProcessorSteps
21
24
  extend Dry::Initializer
22
25
 
23
- STEPS_IN_ORDER = %i[key_coercer filter_schema value_coercer rule_applier].freeze
24
-
25
26
  option :steps, default: -> { EMPTY_HASH.dup }
26
27
  option :before_steps, default: -> { EMPTY_HASH.dup }
27
28
  option :after_steps, default: -> { EMPTY_HASH.dup }
@@ -35,13 +36,29 @@ module Dry
35
36
  # @api public
36
37
  def call(result)
37
38
  STEPS_IN_ORDER.each do |name|
38
- before_steps[name]&.each { |step| process_step(step, result) }
39
- process_step(steps[name], result)
40
- after_steps[name]&.each { |step| process_step(step, result) }
39
+ before_steps[name]&.each { |step| step&.(result) }
40
+ steps[name]&.(result)
41
+ after_steps[name]&.each { |step| step&.(result) }
41
42
  end
43
+
42
44
  result
43
45
  end
44
46
 
47
+ # @api private
48
+ def rule_applier
49
+ @rule_applier ||= steps[:rule_applier].executor
50
+ end
51
+
52
+ # @api private
53
+ def key_map
54
+ @key_map ||= self[:key_coercer].executor.key_map
55
+ end
56
+
57
+ # @api private
58
+ def type_schema
59
+ @type_schema ||= steps[:value_coercer].executor.type_schema
60
+ end
61
+
45
62
  # Returns step by name
46
63
  #
47
64
  # @param [Symbol] name The step name
@@ -57,8 +74,7 @@ module Dry
57
74
  #
58
75
  # @api public
59
76
  def []=(name, value)
60
- validate_step_name(name)
61
- steps[name] = value
77
+ steps[name] = Step.new(type: :core, name: name, executor: value)
62
78
  end
63
79
 
64
80
  # Add passed block before mentioned step
@@ -69,9 +85,8 @@ module Dry
69
85
  #
70
86
  # @api public
71
87
  def after(name, &block)
72
- validate_step_name(name)
73
88
  after_steps[name] ||= EMPTY_ARRAY.dup
74
- after_steps[name] << block.to_proc
89
+ after_steps[name] << Step.new(type: :after, name: name, executor: block)
75
90
  self
76
91
  end
77
92
 
@@ -83,33 +98,41 @@ module Dry
83
98
  #
84
99
  # @api public
85
100
  def before(name, &block)
86
- validate_step_name(name)
87
101
  before_steps[name] ||= EMPTY_ARRAY.dup
88
- before_steps[name] << block.to_proc
102
+ before_steps[name] << Step.new(type: :before, name: name, executor: block)
89
103
  self
90
104
  end
91
105
 
92
- # @api private
93
- def process_step(step, result)
94
- return unless step
95
-
96
- output = step.(result)
97
- result.replace(output) if output.is_a?(::Hash)
106
+ # Stacks callback steps and returns new ProcessorSteps instance
107
+ #
108
+ # @param [ProcessorSteps] other
109
+ #
110
+ # @return [ProcessorSteps]
111
+ #
112
+ # @api public
113
+ def merge(other)
114
+ ProcessorSteps.new(
115
+ before_steps: merge_callbacks(before_steps, other.before_steps),
116
+ after_steps: merge_callbacks(after_steps, other.after_steps)
117
+ )
98
118
  end
99
119
 
100
120
  # @api private
101
- def validate_step_name(name)
102
- return if STEPS_IN_ORDER.include?(name)
103
-
104
- raise ArgumentError, "Undefined step name #{name}. Available names: #{STEPS_IN_ORDER}"
121
+ def merge_callbacks(left, right)
122
+ left.merge(right) do |_key, oldval, newval|
123
+ oldval + newval
124
+ end
105
125
  end
106
126
 
107
127
  # @api private
108
- def initialize_copy(source)
109
- super
110
- @steps = source.steps.dup
111
- @before_steps = source.before_steps.dup
112
- @after_steps = source.after_steps.dup
128
+ def import_callbacks(path, other)
129
+ other.before_steps.each do |name, steps|
130
+ (before_steps[name] ||= []).concat(steps.map { |step| step.scoped(path) })
131
+ end
132
+
133
+ other.after_steps.each do |name, steps|
134
+ (after_steps[name] ||= []).concat(steps.map { |step| step.scoped(path) })
135
+ end
113
136
  end
114
137
  end
115
138
  end
@@ -1,9 +1,9 @@
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'
6
+ require "dry/schema/path"
7
7
 
8
8
  module Dry
9
9
  module Schema
@@ -32,12 +32,43 @@ module Dry
32
32
  option :message_compiler
33
33
 
34
34
  # @api private
35
- def self.new(*args)
35
+ option :parent, default: -> { nil }
36
+
37
+ # @api private
38
+ def self.new(*, **)
36
39
  result = super
37
- yield(result)
40
+ yield(result) if block_given?
38
41
  result.freeze
39
42
  end
40
43
 
44
+ # Return a new result scoped to a specific path
45
+ #
46
+ # @param path [Symbol, Array, Path]
47
+ #
48
+ # @return [Result]
49
+ #
50
+ # @api private
51
+ def at(path, &block)
52
+ new(Path[path].reduce(output) { |a, e| a[e] }, parent: self, &block)
53
+ end
54
+
55
+ # @api private
56
+ def new(output, **opts, &block)
57
+ self.class.new(
58
+ output,
59
+ message_compiler: message_compiler,
60
+ results: results,
61
+ **opts,
62
+ &block
63
+ )
64
+ end
65
+
66
+ # @api private
67
+ def update(hash)
68
+ output.update(hash)
69
+ self
70
+ end
71
+
41
72
  # @api private
42
73
  def replace(hash)
43
74
  @output = hash
@@ -90,7 +121,7 @@ module Dry
90
121
  #
91
122
  # @api public
92
123
  def success?
93
- results.empty?
124
+ result_ast.empty?
94
125
  end
95
126
 
96
127
  # Check if the result is not successful
@@ -136,6 +167,22 @@ module Dry
136
167
  "#<#{self.class}#{to_h.inspect} errors=#{errors.to_h.inspect}>"
137
168
  end
138
169
 
170
+ if RUBY_VERSION >= "2.7"
171
+ # Pattern matching support
172
+ #
173
+ # @api private
174
+ def deconstruct_keys(_)
175
+ output
176
+ end
177
+ end
178
+
179
+ # Add a new error AST node
180
+ #
181
+ # @api private
182
+ def add_error(node)
183
+ result_ast << node
184
+ end
185
+
139
186
  private
140
187
 
141
188
  # A list of failure ASTs produced by rule result objects
@@ -1,12 +1,12 @@
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/config'
7
- require 'dry/schema/result'
8
- require 'dry/schema/messages'
9
- require 'dry/schema/message_compiler'
5
+ require "dry/schema/constants"
6
+ require "dry/schema/config"
7
+ require "dry/schema/result"
8
+ require "dry/schema/messages"
9
+ require "dry/schema/message_compiler"
10
10
 
11
11
  module Dry
12
12
  module Schema
@@ -20,7 +20,7 @@ module Dry
20
20
  param :rules
21
21
 
22
22
  # @api private
23
- option :config, default: -> { Config.new }
23
+ option :config, default: -> { Schema.config.dup }
24
24
 
25
25
  # @api private
26
26
  option :message_compiler, default: -> { MessageCompiler.new(Messages.setup(config.messages)) }
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/constants"
4
+ require "dry/schema/path"
5
+
6
+ module Dry
7
+ module Schema
8
+ # @api private
9
+ class Step
10
+ # @api private
11
+ attr_reader :name
12
+
13
+ # @api private
14
+ attr_reader :type
15
+
16
+ # @api private
17
+ attr_reader :executor
18
+
19
+ # @api private
20
+ class Scoped
21
+ # @api private
22
+ attr_reader :path
23
+
24
+ # @api private
25
+ attr_reader :step
26
+
27
+ # @api private
28
+ def initialize(path, step)
29
+ @path = Path[path]
30
+ @step = step
31
+ end
32
+
33
+ # @api private
34
+ def scoped(new_path)
35
+ self.class.new(Path[[*new_path, *path]], step)
36
+ end
37
+
38
+ # @api private
39
+ def call(result)
40
+ result.at(path) do |scoped_result|
41
+ output = step.(scoped_result).to_h
42
+ target = Array(path)[0..-2].reduce(result) { |a, e| a[e] }
43
+
44
+ target.update(path.last => output)
45
+ end
46
+ end
47
+ end
48
+
49
+ # @api private
50
+ def initialize(type:, name:, executor:)
51
+ @type = type
52
+ @name = name
53
+ @executor = executor
54
+ validate_name(name)
55
+ end
56
+
57
+ # @api private
58
+ def call(result)
59
+ output = executor.(result)
60
+ result.replace(output) if output.is_a?(Hash)
61
+ output
62
+ end
63
+
64
+ # @api private
65
+ def scoped(path)
66
+ Scoped.new(path, self)
67
+ end
68
+
69
+ private
70
+
71
+ # @api private
72
+ def validate_name(name)
73
+ return if STEPS_IN_ORDER.include?(name)
74
+
75
+ raise ArgumentError, "Undefined step name #{name}. Available names: #{STEPS_IN_ORDER}"
76
+ end
77
+ end
78
+ end
79
+ end