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,196 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/cache'
3
+ require "dry/types/predicate_inferrer"
4
4
 
5
5
  module Dry
6
6
  module Schema
7
- # PredicateInferrer is used internally by `Macros::Value`
8
- # for inferring type-check predicates from type specs.
9
- #
10
7
  # @api private
11
- class PredicateInferrer
12
- extend Dry::Core::Cache
8
+ class PredicateInferrer < ::Dry::Types::PredicateInferrer
9
+ Compiler = ::Class.new(superclass::Compiler)
13
10
 
14
- TYPE_TO_PREDICATE = {
15
- DateTime => :date_time?,
16
- FalseClass => :false?,
17
- Integer => :int?,
18
- NilClass => :nil?,
19
- String => :str?,
20
- TrueClass => :true?,
21
- BigDecimal => :decimal?
22
- }.freeze
23
-
24
- REDUCED_TYPES = {
25
- [[[:true?], [:false?]]] => %i[bool?]
26
- }.freeze
27
-
28
- HASH = %i[hash?].freeze
29
-
30
- ARRAY = %i[array?].freeze
31
-
32
- NIL = %i[nil?].freeze
33
-
34
- # Compiler reduces type AST into a list of predicates
35
- #
36
- # @api private
37
- class Compiler
38
- # @return [PredicateRegistry]
39
- # @api private
40
- attr_reader :registry
41
-
42
- # @api private
43
- def initialize(registry)
44
- @registry = registry
45
- end
46
-
47
- # @api private
48
- def infer_predicate(type)
49
- [TYPE_TO_PREDICATE.fetch(type) { :"#{type.name.split('::').last.downcase}?" }]
50
- end
51
-
52
- # @api private
53
- def visit(node)
54
- meth, rest = node
55
- public_send(:"visit_#{meth}", rest)
56
- end
57
-
58
- # @api private
59
- def visit_nominal(node)
60
- type = node[0]
61
- predicate = infer_predicate(type)
62
-
63
- if registry.key?(predicate[0])
64
- predicate
65
- else
66
- [type?: type]
67
- end
68
- end
69
-
70
- # @api private
71
- def visit_hash(_)
72
- HASH
73
- end
74
-
75
- # @api private
76
- def visit_array(_)
77
- ARRAY
78
- end
79
-
80
- # @api private
81
- def visit_lax(node)
82
- visit(node)
83
- end
84
-
85
- # @api private
86
- def visit_constructor(node)
87
- other, * = node
88
- visit(other)
89
- end
90
-
91
- # @api private
92
- def visit_enum(node)
93
- other, * = node
94
- visit(other)
95
- end
96
-
97
- # @api private
98
- def visit_sum(node)
99
- left_node, right_node, = node
100
- left = visit(left_node)
101
- right = visit(right_node)
102
-
103
- if left.eql?(NIL)
104
- right
105
- else
106
- [[left, right]]
107
- end
108
- end
109
-
110
- # @api private
111
- def visit_constrained(node)
112
- other, rules = node
113
- predicates = visit(rules)
114
-
115
- if predicates.empty?
116
- visit(other)
117
- else
118
- [*visit(other), *merge_predicates(predicates)]
119
- end
120
- end
121
-
122
- # @api private
123
- def visit_any(_)
124
- EMPTY_ARRAY
125
- end
126
-
127
- # @api private
128
- def visit_and(node)
129
- left, right = node
130
- visit(left) + visit(right)
131
- end
132
-
133
- # @api private
134
- def visit_predicate(node)
135
- pred, args = node
136
-
137
- if pred.equal?(:type?)
138
- EMPTY_ARRAY
139
- elsif registry.key?(pred)
140
- *curried, _ = args
141
- values = curried.map { |_, v| v }
142
-
143
- if values.empty?
144
- [pred]
145
- else
146
- [pred => values[0]]
147
- end
148
- else
149
- EMPTY_ARRAY
150
- end
151
- end
152
-
153
- private
154
-
155
- # @api private
156
- def merge_predicates(nodes)
157
- preds, merged = nodes.each_with_object([[], {}]) do |predicate, (ps, h)|
158
- if predicate.is_a?(::Hash)
159
- h.update(predicate)
160
- else
161
- ps << predicate
162
- end
163
- end
164
-
165
- merged.empty? ? preds : [*preds, merged]
166
- end
167
- end
168
-
169
- # @return [Compiler]
170
- # @api private
171
- attr_reader :compiler
172
-
173
- # @api private
174
- def initialize(registry)
11
+ def initialize(registry = PredicateRegistry.new)
175
12
  @compiler = Compiler.new(registry)
176
13
  end
177
-
178
- # Infer predicate identifier from the provided type
179
- #
180
- # @return [Symbol]
181
- #
182
- # @api private
183
- def [](type)
184
- self.class.fetch_or_store(type.hash) do
185
- predicates = compiler.visit(type.to_ast)
186
-
187
- if predicates.is_a?(Hash)
188
- predicates
189
- else
190
- REDUCED_TYPES[predicates] || predicates
191
- end
192
- end
193
- end
194
14
  end
195
15
  end
196
16
  end
@@ -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
@@ -68,7 +71,7 @@ module Dry
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