dry-schema 1.3.4 → 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 (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
@@ -1,35 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/logic/predicates'
3
+ require "dry/logic/predicates"
4
+ require "dry/types/predicate_registry"
4
5
 
5
6
  module Dry
6
7
  module Schema
7
8
  # A registry with predicate objects from `Dry::Logic::Predicates`
8
9
  #
9
10
  # @api private
10
- class PredicateRegistry
11
- # @api private
12
- attr_reader :predicates
13
-
14
- # @api private
15
- attr_reader :has_predicate
16
-
17
- # @api private
18
- def initialize(predicates = Dry::Logic::Predicates)
19
- @predicates = predicates
20
- @has_predicate = ::Kernel.instance_method(:respond_to?).bind(@predicates)
21
- end
22
-
23
- # @api private
24
- def [](name)
25
- predicates[name]
26
- end
27
-
28
- # @api private
29
- def key?(name)
30
- has_predicate.(name)
31
- end
32
-
11
+ class PredicateRegistry < Dry::Types::PredicateRegistry
33
12
  # @api private
34
13
  def arg_list(name, *values)
35
14
  predicate = self[name]
@@ -1,99 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/cache'
3
+ require "dry/types/primitive_inferrer"
4
4
 
5
5
  module Dry
6
6
  module Schema
7
- # PrimitiveInferrer is used internally by `Macros::Filled`
8
- # for inferring a list of possible primitives that a given
9
- # type can handle.
10
- #
11
7
  # @api private
12
- class PrimitiveInferrer
13
- extend Dry::Core::Cache
8
+ class PrimitiveInferrer < ::Dry::Types::PrimitiveInferrer
9
+ Compiler = ::Class.new(superclass::Compiler)
14
10
 
15
- # Compiler reduces type AST into a list of primitives
16
- #
17
- # @api private
18
- class Compiler
19
- # @api private
20
- def visit(node)
21
- meth, rest = node
22
- public_send(:"visit_#{meth}", rest)
23
- end
24
-
25
- # @api private
26
- def visit_nominal(node)
27
- type, _ = node
28
- type
29
- end
30
-
31
- # @api private
32
- def visit_hash(_)
33
- Hash
34
- end
35
- alias_method :visit_schema, :visit_hash
36
-
37
- # @api private
38
- def visit_array(_)
39
- Array
40
- end
41
-
42
- # @api private
43
- def visit_lax(node)
44
- visit(node)
45
- end
46
-
47
- # @api private
48
- def visit_constructor(node)
49
- other, * = node
50
- visit(other)
51
- end
52
-
53
- # @api private
54
- def visit_enum(node)
55
- other, * = node
56
- visit(other)
57
- end
58
-
59
- # @api private
60
- def visit_sum(node)
61
- left, right = node
62
-
63
- [visit(left), visit(right)].flatten(1)
64
- end
65
-
66
- # @api private
67
- def visit_constrained(node)
68
- other, * = node
69
- visit(other)
70
- end
71
-
72
- # @api private
73
- def visit_any(_)
74
- Object
75
- end
76
- end
77
-
78
- # @return [Compiler]
79
- # @api private
80
- attr_reader :compiler
81
-
82
- # @api private
83
11
  def initialize
84
12
  @compiler = Compiler.new
85
13
  end
86
-
87
- # Infer predicate identifier from the provided type
88
- #
89
- # @return [Symbol]
90
- #
91
- # @api private
92
- def [](type)
93
- self.class.fetch_or_store(type.hash) do
94
- Array(compiler.visit(type.to_ast)).freeze
95
- end
96
- end
97
14
  end
98
15
  end
99
16
  end
@@ -1,25 +1,22 @@
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/rule_applier'
9
- require 'dry/schema/key_coercer'
10
- 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"
11
13
 
12
14
  module Dry
13
15
  module Schema
14
16
  # Processes input data using objects configured within the DSL
17
+ # Processing is split into steps represented by `ProcessorSteps`.
15
18
  #
16
- # Processing is split into 4 main steps:
17
- #
18
- # 1. Prepare input hash using a key map
19
- # 2. Apply pre-coercion filtering rules (optional step, used only when `filter` was used)
20
- # 3. Apply value coercions based on type specifications
21
- # 4. Apply rules
22
- #
19
+ # @see ProcessorSteps
23
20
  # @see Params
24
21
  # @see JSON
25
22
  #
@@ -28,11 +25,13 @@ module Dry
28
25
  extend Dry::Initializer
29
26
  extend Dry::Configurable
30
27
 
28
+ include Dry::Logic::Operators
29
+
31
30
  setting :key_map_type
32
31
  setting :type_registry_namespace, :strict
33
32
  setting :filter_empty_string, false
34
33
 
35
- option :steps, default: -> { EMPTY_ARRAY.dup }
34
+ option :steps, default: -> { ProcessorSteps.new }
36
35
 
37
36
  option :schema_dsl
38
37
 
@@ -66,27 +65,17 @@ module Dry
66
65
  # @api public
67
66
  def new(options = nil, &block)
68
67
  if options || block
69
- processor = super
68
+ processor = super(**(options || EMPTY_HASH))
70
69
  yield(processor) if block
71
70
  processor
72
71
  elsif definition
73
72
  definition.call
74
73
  else
75
- raise ArgumentError, 'Cannot create a schema without a definition'
74
+ raise ArgumentError, "Cannot create a schema without a definition"
76
75
  end
77
76
  end
78
77
  end
79
78
 
80
- # Append a step
81
- #
82
- # @return [Processor]
83
- #
84
- # @api private
85
- def <<(step)
86
- steps << step
87
- self
88
- end
89
-
90
79
  # Apply processing steps to the provided input
91
80
  #
92
81
  # @param [Hash] input
@@ -95,31 +84,36 @@ module Dry
95
84
  #
96
85
  # @api public
97
86
  def call(input)
98
- Result.new(input, message_compiler: message_compiler) do |result|
99
- steps.each do |step|
100
- output = step.(result)
101
- result.replace(output) if output.is_a?(::Hash)
102
- end
87
+ Result.new(input, input: input, message_compiler: message_compiler) do |result|
88
+ steps.call(result)
103
89
  end
104
90
  end
105
91
  alias_method :[], :call
106
92
 
107
- # 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
108
100
  #
109
- # @return [Proc]
101
+ # @param [Processor] other
102
+ #
103
+ # @return [Processor, Params, JSON]
110
104
  #
111
105
  # @api public
112
- def to_proc
113
- ->(input) { call(input) }
106
+ def merge(other)
107
+ schema_dsl.merge(other.schema_dsl).()
114
108
  end
115
109
 
116
- # Return the key map
110
+ # Return a proc that acts like a schema object
117
111
  #
118
- # @return [KeyMap]
112
+ # @return [Proc]
119
113
  #
120
114
  # @api public
121
- def key_map
122
- @key_map ||= steps.detect { |s| s.is_a?(KeyCoercer) }.key_map
115
+ def to_proc
116
+ ->(input) { call(input) }
123
117
  end
124
118
 
125
119
  # Return string represntation
@@ -133,15 +127,32 @@ module Dry
133
127
  STR
134
128
  end
135
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
+
136
139
  # Return the type schema
137
140
  #
138
141
  # @return [Dry::Types::Safe]
139
142
  #
140
143
  # @api private
141
144
  def type_schema
142
- @type_schema ||= steps.detect { |s| s.is_a?(ValueCoercer) }.type_schema
145
+ steps.type_schema
143
146
  end
144
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
+
145
156
  # Return the rules config
146
157
  #
147
158
  # @return [Dry::Types::Config]
@@ -154,9 +165,10 @@ module Dry
154
165
  # Return AST representation of the rules
155
166
  #
156
167
  # @api private
157
- def to_ast
168
+ def to_ast(*)
158
169
  rule_applier.to_ast
159
170
  end
171
+ alias_method :ast, :to_ast
160
172
 
161
173
  # Return the message compiler
162
174
  #
@@ -176,14 +188,6 @@ module Dry
176
188
  rule_applier.rules
177
189
  end
178
190
 
179
- # Return the rule applier
180
- #
181
- # @api private
182
- def rule_applier
183
- @rule_applier ||= steps.last
184
- end
185
- alias_method :to_rule, :rule_applier
186
-
187
191
  # Check if there are filter rules
188
192
  #
189
193
  # @api private
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/initializer"
4
+
5
+ require "dry/schema/constants"
6
+ require "dry/schema/step"
7
+
8
+ module Dry
9
+ module Schema
10
+ # Steps for the Dry::Schema::Processor
11
+ #
12
+ # There are 4 main steps:
13
+ #
14
+ # 1. `key_coercer` - Prepare input hash using a key map
15
+ # 2. `filter_schema` - Apply pre-coercion filtering rules
16
+ # (optional step, used only when `filter` was used)
17
+ # 3. `value_coercer` - Apply value coercions based on type specifications
18
+ # 4. `rule_applier` - Apply rules
19
+ #
20
+ # @see Processor
21
+ #
22
+ # @api public
23
+ class ProcessorSteps
24
+ extend Dry::Initializer
25
+
26
+ option :steps, default: -> { EMPTY_HASH.dup }
27
+ option :before_steps, default: -> { EMPTY_HASH.dup }
28
+ option :after_steps, default: -> { EMPTY_HASH.dup }
29
+
30
+ # Executes steps and callbacks in order
31
+ #
32
+ # @param [Result] result
33
+ #
34
+ # @return [Result]
35
+ #
36
+ # @api public
37
+ def call(result)
38
+ STEPS_IN_ORDER.each do |name|
39
+ before_steps[name]&.each { |step| step&.(result) }
40
+ steps[name]&.(result)
41
+ after_steps[name]&.each { |step| step&.(result) }
42
+ end
43
+
44
+ result
45
+ end
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
+
62
+ # Returns step by name
63
+ #
64
+ # @param [Symbol] name The step name
65
+ #
66
+ # @api public
67
+ def [](name)
68
+ steps[name]
69
+ end
70
+
71
+ # Sets step by name
72
+ #
73
+ # @param [Symbol] name The step name
74
+ #
75
+ # @api public
76
+ def []=(name, value)
77
+ steps[name] = Step.new(type: :core, name: name, executor: value)
78
+ end
79
+
80
+ # Add passed block before mentioned step
81
+ #
82
+ # @param [Symbol] name The step name
83
+ #
84
+ # @return [ProcessorSteps]
85
+ #
86
+ # @api public
87
+ def after(name, &block)
88
+ after_steps[name] ||= EMPTY_ARRAY.dup
89
+ after_steps[name] << Step.new(type: :after, name: name, executor: block)
90
+ self
91
+ end
92
+
93
+ # Add passed block before mentioned step
94
+ #
95
+ # @param [Symbol] name The step name
96
+ #
97
+ # @return [ProcessorSteps]
98
+ #
99
+ # @api public
100
+ def before(name, &block)
101
+ before_steps[name] ||= EMPTY_ARRAY.dup
102
+ before_steps[name] << Step.new(type: :before, name: name, executor: block)
103
+ self
104
+ end
105
+
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
+ )
118
+ end
119
+
120
+ # @api private
121
+ def merge_callbacks(left, right)
122
+ left.merge(right) do |_key, oldval, newval|
123
+ oldval + newval
124
+ end
125
+ end
126
+
127
+ # @api private
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
136
+ end
137
+ end
138
+ end
139
+ end