dry-validation 0.13.3 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (206) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +233 -12
  3. data/LICENSE +1 -1
  4. data/README.md +13 -9
  5. data/config/errors.yml +3 -88
  6. data/lib/dry-validation.rb +2 -0
  7. data/lib/dry/validation.rb +47 -28
  8. data/lib/dry/validation/config.rb +24 -0
  9. data/lib/dry/validation/constants.rb +43 -0
  10. data/lib/dry/validation/contract.rb +160 -0
  11. data/lib/dry/validation/contract/class_interface.rb +223 -0
  12. data/lib/dry/validation/evaluator.rb +197 -0
  13. data/lib/dry/validation/extensions/hints.rb +69 -0
  14. data/lib/dry/validation/extensions/monads.rb +23 -7
  15. data/lib/dry/validation/extensions/predicates_as_macros.rb +75 -0
  16. data/lib/dry/validation/failures.rb +58 -0
  17. data/lib/dry/validation/function.rb +44 -0
  18. data/lib/dry/validation/macro.rb +38 -0
  19. data/lib/dry/validation/macros.rb +104 -0
  20. data/lib/dry/validation/message.rb +79 -79
  21. data/lib/dry/validation/message_set.rb +108 -88
  22. data/lib/dry/validation/messages/resolver.rb +79 -0
  23. data/lib/dry/validation/result.rb +154 -42
  24. data/lib/dry/validation/rule.rb +129 -0
  25. data/lib/dry/validation/schema_ext.rb +46 -0
  26. data/lib/dry/validation/values.rb +94 -0
  27. data/lib/dry/validation/version.rb +3 -1
  28. metadata +41 -336
  29. data/.codeclimate.yml +0 -17
  30. data/.gitignore +0 -9
  31. data/.rspec +0 -3
  32. data/.travis.yml +0 -29
  33. data/CONTRIBUTING.md +0 -31
  34. data/Gemfile +0 -25
  35. data/Rakefile +0 -22
  36. data/benchmarks/benchmark_form_invalid.rb +0 -64
  37. data/benchmarks/benchmark_form_valid.rb +0 -64
  38. data/benchmarks/benchmark_schema_invalid_huge.rb +0 -52
  39. data/benchmarks/profile_schema_call_invalid.rb +0 -20
  40. data/benchmarks/profile_schema_call_valid.rb +0 -20
  41. data/benchmarks/profile_schema_definition.rb +0 -14
  42. data/benchmarks/profile_schema_huge_invalid.rb +0 -30
  43. data/benchmarks/profile_schema_messages_invalid.rb +0 -20
  44. data/benchmarks/suite.rb +0 -5
  45. data/dry-validation.gemspec +0 -28
  46. data/examples/basic.rb +0 -15
  47. data/examples/each.rb +0 -14
  48. data/examples/json.rb +0 -12
  49. data/examples/multiple.rb +0 -27
  50. data/examples/nested.rb +0 -22
  51. data/examples/params.rb +0 -11
  52. data/lib/dry/validation/compat/form.rb +0 -67
  53. data/lib/dry/validation/deprecations.rb +0 -24
  54. data/lib/dry/validation/executor.rb +0 -91
  55. data/lib/dry/validation/extensions.rb +0 -7
  56. data/lib/dry/validation/extensions/struct.rb +0 -32
  57. data/lib/dry/validation/input_processor_compiler.rb +0 -137
  58. data/lib/dry/validation/input_processor_compiler/json.rb +0 -45
  59. data/lib/dry/validation/input_processor_compiler/params.rb +0 -49
  60. data/lib/dry/validation/input_processor_compiler/sanitizer.rb +0 -47
  61. data/lib/dry/validation/message_compiler.rb +0 -188
  62. data/lib/dry/validation/message_compiler/visitor_opts.rb +0 -37
  63. data/lib/dry/validation/messages.rb +0 -14
  64. data/lib/dry/validation/messages/abstract.rb +0 -119
  65. data/lib/dry/validation/messages/i18n.rb +0 -47
  66. data/lib/dry/validation/messages/namespaced.rb +0 -39
  67. data/lib/dry/validation/messages/yaml.rb +0 -61
  68. data/lib/dry/validation/predicate_registry.rb +0 -115
  69. data/lib/dry/validation/predicates.rb +0 -19
  70. data/lib/dry/validation/schema.rb +0 -126
  71. data/lib/dry/validation/schema/check.rb +0 -37
  72. data/lib/dry/validation/schema/class_interface.rb +0 -190
  73. data/lib/dry/validation/schema/deprecated.rb +0 -30
  74. data/lib/dry/validation/schema/dsl.rb +0 -118
  75. data/lib/dry/validation/schema/form.rb +0 -9
  76. data/lib/dry/validation/schema/json.rb +0 -21
  77. data/lib/dry/validation/schema/key.rb +0 -71
  78. data/lib/dry/validation/schema/params.rb +0 -22
  79. data/lib/dry/validation/schema/rule.rb +0 -202
  80. data/lib/dry/validation/schema/value.rb +0 -211
  81. data/lib/dry/validation/schema_compiler.rb +0 -81
  82. data/lib/dry/validation/template.rb +0 -66
  83. data/lib/dry/validation/type_specs.rb +0 -70
  84. data/spec/extensions/monads/result_spec.rb +0 -40
  85. data/spec/extensions/struct/schema_spec.rb +0 -32
  86. data/spec/fixtures/locales/en.yml +0 -8
  87. data/spec/fixtures/locales/pl.yml +0 -22
  88. data/spec/integration/custom_error_messages_spec.rb +0 -54
  89. data/spec/integration/custom_predicates_spec.rb +0 -228
  90. data/spec/integration/hints_spec.rb +0 -170
  91. data/spec/integration/injecting_rules_spec.rb +0 -30
  92. data/spec/integration/json/defining_base_schema_spec.rb +0 -41
  93. data/spec/integration/localized_error_messages_spec.rb +0 -72
  94. data/spec/integration/message_compiler_spec.rb +0 -405
  95. data/spec/integration/messages/i18n_spec.rb +0 -104
  96. data/spec/integration/optional_keys_spec.rb +0 -28
  97. data/spec/integration/params/predicates/array_spec.rb +0 -287
  98. data/spec/integration/params/predicates/empty_spec.rb +0 -263
  99. data/spec/integration/params/predicates/eql_spec.rb +0 -327
  100. data/spec/integration/params/predicates/even_spec.rb +0 -455
  101. data/spec/integration/params/predicates/excluded_from_spec.rb +0 -455
  102. data/spec/integration/params/predicates/excludes_spec.rb +0 -391
  103. data/spec/integration/params/predicates/false_spec.rb +0 -455
  104. data/spec/integration/params/predicates/filled_spec.rb +0 -467
  105. data/spec/integration/params/predicates/format_spec.rb +0 -454
  106. data/spec/integration/params/predicates/gt_spec.rb +0 -519
  107. data/spec/integration/params/predicates/gteq_spec.rb +0 -519
  108. data/spec/integration/params/predicates/included_in_spec.rb +0 -455
  109. data/spec/integration/params/predicates/includes_spec.rb +0 -391
  110. data/spec/integration/params/predicates/key_spec.rb +0 -67
  111. data/spec/integration/params/predicates/lt_spec.rb +0 -519
  112. data/spec/integration/params/predicates/lteq_spec.rb +0 -519
  113. data/spec/integration/params/predicates/max_size_spec.rb +0 -391
  114. data/spec/integration/params/predicates/min_size_spec.rb +0 -391
  115. data/spec/integration/params/predicates/none_spec.rb +0 -265
  116. data/spec/integration/params/predicates/not_eql_spec.rb +0 -327
  117. data/spec/integration/params/predicates/odd_spec.rb +0 -455
  118. data/spec/integration/params/predicates/size/fixed_spec.rb +0 -393
  119. data/spec/integration/params/predicates/size/range_spec.rb +0 -396
  120. data/spec/integration/params/predicates/true_spec.rb +0 -455
  121. data/spec/integration/params/predicates/type_spec.rb +0 -391
  122. data/spec/integration/result_spec.rb +0 -81
  123. data/spec/integration/schema/array_schema_spec.rb +0 -59
  124. data/spec/integration/schema/check_rules_spec.rb +0 -119
  125. data/spec/integration/schema/check_with_nested_el_spec.rb +0 -37
  126. data/spec/integration/schema/check_with_nth_el_spec.rb +0 -25
  127. data/spec/integration/schema/default_settings_spec.rb +0 -11
  128. data/spec/integration/schema/defining_base_schema_spec.rb +0 -41
  129. data/spec/integration/schema/dynamic_predicate_args_spec.rb +0 -43
  130. data/spec/integration/schema/each_with_set_spec.rb +0 -70
  131. data/spec/integration/schema/extending_dsl_spec.rb +0 -27
  132. data/spec/integration/schema/form_spec.rb +0 -236
  133. data/spec/integration/schema/hash_schema_spec.rb +0 -47
  134. data/spec/integration/schema/inheriting_schema_spec.rb +0 -31
  135. data/spec/integration/schema/input_processor_spec.rb +0 -46
  136. data/spec/integration/schema/json/explicit_types_spec.rb +0 -157
  137. data/spec/integration/schema/json_spec.rb +0 -163
  138. data/spec/integration/schema/macros/confirmation_spec.rb +0 -35
  139. data/spec/integration/schema/macros/each_spec.rb +0 -268
  140. data/spec/integration/schema/macros/filled_spec.rb +0 -87
  141. data/spec/integration/schema/macros/input_spec.rb +0 -139
  142. data/spec/integration/schema/macros/maybe_spec.rb +0 -99
  143. data/spec/integration/schema/macros/rule_spec.rb +0 -75
  144. data/spec/integration/schema/macros/value_spec.rb +0 -119
  145. data/spec/integration/schema/macros/when_spec.rb +0 -62
  146. data/spec/integration/schema/nested_schemas_spec.rb +0 -236
  147. data/spec/integration/schema/nested_values_spec.rb +0 -46
  148. data/spec/integration/schema/not_spec.rb +0 -34
  149. data/spec/integration/schema/numbers_spec.rb +0 -19
  150. data/spec/integration/schema/option_with_default_spec.rb +0 -64
  151. data/spec/integration/schema/or_spec.rb +0 -87
  152. data/spec/integration/schema/params/defining_base_schema_spec.rb +0 -41
  153. data/spec/integration/schema/params/explicit_types_spec.rb +0 -195
  154. data/spec/integration/schema/params_spec.rb +0 -234
  155. data/spec/integration/schema/predicate_verification_spec.rb +0 -9
  156. data/spec/integration/schema/predicates/array_spec.rb +0 -295
  157. data/spec/integration/schema/predicates/custom_spec.rb +0 -103
  158. data/spec/integration/schema/predicates/empty_spec.rb +0 -263
  159. data/spec/integration/schema/predicates/eql_spec.rb +0 -327
  160. data/spec/integration/schema/predicates/even_spec.rb +0 -455
  161. data/spec/integration/schema/predicates/excluded_from/array_spec.rb +0 -459
  162. data/spec/integration/schema/predicates/excluded_from/range_spec.rb +0 -459
  163. data/spec/integration/schema/predicates/excludes_spec.rb +0 -391
  164. data/spec/integration/schema/predicates/filled_spec.rb +0 -467
  165. data/spec/integration/schema/predicates/format_spec.rb +0 -455
  166. data/spec/integration/schema/predicates/gt_spec.rb +0 -519
  167. data/spec/integration/schema/predicates/gteq_spec.rb +0 -519
  168. data/spec/integration/schema/predicates/hash_spec.rb +0 -69
  169. data/spec/integration/schema/predicates/included_in/array_spec.rb +0 -459
  170. data/spec/integration/schema/predicates/included_in/range_spec.rb +0 -459
  171. data/spec/integration/schema/predicates/includes_spec.rb +0 -391
  172. data/spec/integration/schema/predicates/key_spec.rb +0 -88
  173. data/spec/integration/schema/predicates/lt_spec.rb +0 -520
  174. data/spec/integration/schema/predicates/lteq_spec.rb +0 -519
  175. data/spec/integration/schema/predicates/max_size_spec.rb +0 -391
  176. data/spec/integration/schema/predicates/min_size_spec.rb +0 -391
  177. data/spec/integration/schema/predicates/none_spec.rb +0 -265
  178. data/spec/integration/schema/predicates/not_eql_spec.rb +0 -391
  179. data/spec/integration/schema/predicates/odd_spec.rb +0 -455
  180. data/spec/integration/schema/predicates/size/fixed_spec.rb +0 -398
  181. data/spec/integration/schema/predicates/size/range_spec.rb +0 -395
  182. data/spec/integration/schema/predicates/type_spec.rb +0 -413
  183. data/spec/integration/schema/reusing_schema_spec.rb +0 -33
  184. data/spec/integration/schema/using_types_spec.rb +0 -135
  185. data/spec/integration/schema/validate_spec.rb +0 -120
  186. data/spec/integration/schema/xor_spec.rb +0 -35
  187. data/spec/integration/schema_builders_spec.rb +0 -17
  188. data/spec/integration/schema_spec.rb +0 -173
  189. data/spec/shared/message_compiler.rb +0 -11
  190. data/spec/shared/predicate_helper.rb +0 -15
  191. data/spec/shared/rule_compiler.rb +0 -8
  192. data/spec/spec_helper.rb +0 -62
  193. data/spec/support/define_struct.rb +0 -25
  194. data/spec/support/matchers.rb +0 -38
  195. data/spec/support/mutant.rb +0 -9
  196. data/spec/support/predicates_integration.rb +0 -7
  197. data/spec/unit/input_processor_compiler/json_spec.rb +0 -283
  198. data/spec/unit/input_processor_compiler/params_spec.rb +0 -328
  199. data/spec/unit/message_compiler/visit_failure_spec.rb +0 -38
  200. data/spec/unit/message_compiler/visit_spec.rb +0 -16
  201. data/spec/unit/message_compiler_spec.rb +0 -7
  202. data/spec/unit/predicate_registry_spec.rb +0 -34
  203. data/spec/unit/schema/key_spec.rb +0 -38
  204. data/spec/unit/schema/rule_spec.rb +0 -42
  205. data/spec/unit/schema/value_spec.rb +0 -131
  206. data/spec/unit/schema_spec.rb +0 -35
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/validation/message'
4
+
5
+ module Dry
6
+ module Validation
7
+ module Messages
8
+ # Resolve translated messages from failure arguments
9
+ #
10
+ # @api public
11
+ class Resolver
12
+ # @!attribute [r] messages
13
+ # @return [Messages::I18n, Messages::YAML] messages backend
14
+ # @api private
15
+ attr_reader :messages
16
+
17
+ # @api private
18
+ def initialize(messages)
19
+ @messages = messages
20
+ end
21
+
22
+ # Resolve Message object from provided args and path
23
+ #
24
+ # This is used internally by contracts when rules are applied
25
+ #
26
+ # @return [Message, Message::Localized]
27
+ #
28
+ # @api public
29
+ def call(message:, tokens:, path:, meta: EMPTY_HASH)
30
+ case message
31
+ when Symbol
32
+ Message[->(**opts) { message(message, path: path, tokens: tokens, **opts) }, path, meta]
33
+ when String
34
+ Message[message, path, meta]
35
+ when Hash
36
+ meta = message.dup
37
+ text = meta.delete(:text)
38
+ call(message: text, tokens: tokens, path: path, meta: meta)
39
+ else
40
+ raise ArgumentError, <<~STR
41
+ +message+ must be either a Symbol, String or Hash (#{message.inspect} given)
42
+ STR
43
+ end
44
+ end
45
+ alias_method :[], :call
46
+
47
+ # Resolve a message
48
+ #
49
+ # @return [String]
50
+ #
51
+ # @api public
52
+ #
53
+ # rubocop:disable Metrics/AbcSize
54
+ def message(rule, tokens: EMPTY_HASH, locale: nil, full: false, path:)
55
+ keys = path.to_a.compact
56
+ msg_opts = tokens.merge(path: keys, locale: locale || messages.default_locale)
57
+
58
+ if keys.empty?
59
+ template, meta = messages["rules.#{rule}", msg_opts]
60
+ else
61
+ template, meta = messages[rule, msg_opts.merge(path: keys.join(DOT))]
62
+ template, meta = messages[rule, msg_opts.merge(path: keys.last)] unless template
63
+ end
64
+
65
+ unless template
66
+ raise MissingMessageError, <<~STR
67
+ Message template for #{rule.inspect} under #{keys.join(DOT).inspect} was not found
68
+ STR
69
+ end
70
+
71
+ text = template.(template.data(tokens))
72
+
73
+ [full ? "#{messages.rule(keys.last, msg_opts)} #{text}" : text, meta]
74
+ end
75
+ # rubocop:enable Metrics/AbcSize
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,82 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent/map'
1
4
  require 'dry/equalizer'
2
5
 
6
+ require 'dry/validation/constants'
7
+ require 'dry/validation/message_set'
8
+ require 'dry/validation/values'
9
+
3
10
  module Dry
4
11
  module Validation
12
+ # Result objects are returned by contracts
13
+ #
14
+ # @api public
5
15
  class Result
6
- include Dry::Equalizer(:output, :errors)
7
- include Enumerable
8
-
9
- attr_reader :output
10
- attr_reader :results
11
- attr_reader :message_compiler
12
- attr_reader :path
13
-
14
- alias_method :to_hash, :output
15
- alias_method :to_h, :output # for MRI 2.0, remove it when drop support
16
+ include Dry::Equalizer(:schema_result, :context, :errors, inspect: false)
17
+
18
+ # Build a new result
19
+ #
20
+ # @param [Dry::Schema::Result] schema_result
21
+ #
22
+ # @api private
23
+ def self.new(schema_result, context = ::Concurrent::Map.new, options = EMPTY_HASH)
24
+ result = super
25
+ yield(result) if block_given?
26
+ result.freeze
27
+ end
16
28
 
17
- def initialize(output, results, message_compiler, path)
18
- @output = output
19
- @results = results
20
- @message_compiler = message_compiler
21
- @path = path
29
+ # Context that's shared between rules
30
+ #
31
+ # @return [Concurrent::Map]
32
+ #
33
+ # @api public
34
+ attr_reader :context
35
+
36
+ # Result from contract's schema
37
+ #
38
+ # @return [Dry::Schema::Result]
39
+ #
40
+ # @api private
41
+ attr_reader :schema_result
42
+
43
+ # Result options
44
+ #
45
+ # @return [Hash]
46
+ #
47
+ # @api private
48
+ attr_reader :options
49
+
50
+ # Initialize a new result
51
+ #
52
+ # @api private
53
+ def initialize(schema_result, context, options)
54
+ @schema_result = schema_result
55
+ @context = context
56
+ @options = options
57
+ @errors = initialize_errors
22
58
  end
23
59
 
24
- def each(&block)
25
- output.each(&block)
60
+ # Return values wrapper with the input processed by schema
61
+ #
62
+ # @return [Values]
63
+ #
64
+ # @api public
65
+ def values
66
+ @values ||= Values.new(schema_result.to_h)
26
67
  end
27
68
 
28
- def [](name)
29
- output.fetch(name)
69
+ # Get error set
70
+ #
71
+ # @!macro errors-options
72
+ # @param [Hash] new_options
73
+ # @option new_options [Symbol] :locale Set locale for messages
74
+ # @option new_options [Boolean] :hints Enable/disable hints
75
+ # @option new_options [Boolean] :full Get messages that include key names
76
+ #
77
+ # @return [MessageSet]
78
+ #
79
+ # @api public
80
+ def errors(new_options = EMPTY_HASH)
81
+ new_options.empty? ? @errors : @errors.with(schema_errors(new_options), new_options)
30
82
  end
31
83
 
84
+ # Check if result is successful
85
+ #
86
+ # @return [Bool]
87
+ #
88
+ # @api public
32
89
  def success?
33
- results.empty?
90
+ @errors.empty?
34
91
  end
35
92
 
93
+ # Check if result is not successful
94
+ #
95
+ # @return [Bool]
96
+ #
97
+ # @api public
36
98
  def failure?
37
99
  !success?
38
100
  end
39
101
 
40
- def messages(options = EMPTY_HASH)
41
- message_set(options).dump
102
+ # Check if values include an error for the provided key
103
+ #
104
+ # @api private
105
+ def error?(key)
106
+ schema_result.error?(key)
42
107
  end
43
108
 
44
- def errors(options = EMPTY_HASH)
45
- message_set(options.merge(hints: false)).dump
109
+ # Check if there's any error for the provided key
110
+ #
111
+ # This does not consider errors from the nested values
112
+ #
113
+ # @api private
114
+ def base_error?(key)
115
+ schema_result.errors.any? { |error|
116
+ key_path = Schema::Path[key]
117
+ err_path = Schema::Path[error.path]
118
+
119
+ return false unless key_path.same_root?(err_path)
120
+
121
+ key_path == err_path
122
+ }
46
123
  end
47
124
 
48
- def hints(options = EMPTY_HASH)
49
- message_set(options.merge(failures: false)).dump
125
+ # Add a new error for the provided key
126
+ #
127
+ # @api private
128
+ def add_error(error)
129
+ @errors.add(error)
130
+ self
50
131
  end
51
132
 
52
- def message_set(options = EMPTY_HASH)
53
- message_compiler.with(options).(result_ast)
133
+ # Read a value under provided key
134
+ #
135
+ # @param [Symbol] key
136
+ #
137
+ # @return [Object]
138
+ #
139
+ # @api public
140
+ def [](key)
141
+ values[key]
54
142
  end
55
143
 
56
- def to_ast
57
- if name
58
- [type, [name, [:set, result_ast]]]
59
- else
60
- ast
61
- end
144
+ # Check if a key was set
145
+ #
146
+ # @param [Symbol] key
147
+ #
148
+ # @return [Bool]
149
+ #
150
+ # @api public
151
+ def key?(key)
152
+ values.key?(key)
153
+ end
154
+
155
+ # Coerce to a hash
156
+ #
157
+ # @api public
158
+ def to_h
159
+ values.to_h
62
160
  end
63
161
 
64
- def ast(*)
65
- [:set, result_ast]
162
+ # Return a string representation
163
+ #
164
+ # @api public
165
+ def inspect
166
+ if context.empty?
167
+ "#<#{self.class}#{to_h} errors=#{errors.to_h}>"
168
+ else
169
+ "#<#{self.class}#{to_h} errors=#{errors.to_h} context=#{context.each.to_h}>"
170
+ end
66
171
  end
67
172
 
68
- def name
69
- Array(path).last
173
+ # Freeze result and its error set
174
+ #
175
+ # @api private
176
+ def freeze
177
+ values.freeze
178
+ errors.freeze
179
+ super
70
180
  end
71
181
 
72
182
  private
73
183
 
74
- def type
75
- success? ? :success : :failure
184
+ # @api private
185
+ def initialize_errors(options = self.options)
186
+ MessageSet.new(schema_errors(options), options)
76
187
  end
77
188
 
78
- def result_ast
79
- @result_ast ||= results.map(&:to_ast)
189
+ # @api private
190
+ def schema_errors(options)
191
+ schema_result.message_set(options).to_a
80
192
  end
81
193
  end
82
194
  end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/equalizer'
4
+
5
+ require 'dry/validation/constants'
6
+ require 'dry/validation/function'
7
+
8
+ module Dry
9
+ module Validation
10
+ # Rules capture configuration and evaluator blocks
11
+ #
12
+ # When a rule is applied, it creates an `Evaluator` using schema result and its
13
+ # block will be evaluated in the context of the evaluator.
14
+ #
15
+ # @see Contract#rule
16
+ #
17
+ # @api public
18
+ class Rule < Function
19
+ include Dry::Equalizer(:keys, :block, inspect: false)
20
+
21
+ # @!attribute [r] keys
22
+ # @return [Array<Symbol, String, Hash>]
23
+ # @api private
24
+ option :keys
25
+
26
+ # @!attribute [r] macros
27
+ # @return [Array<Symbol>]
28
+ # @api private
29
+ option :macros, default: proc { EMPTY_ARRAY.dup }
30
+
31
+ # Evaluate the rule within the provided context
32
+ #
33
+ # @param [Contract] contract
34
+ # @param [Result] result
35
+ #
36
+ # @api private
37
+ def call(contract, result)
38
+ Evaluator.new(
39
+ contract,
40
+ keys: keys,
41
+ macros: macros,
42
+ block_options: block_options,
43
+ result: result,
44
+ values: result.values,
45
+ _context: result.context,
46
+ &block
47
+ )
48
+ end
49
+
50
+ # Define which macros should be executed
51
+ #
52
+ # @see Contract#rule
53
+ # @return [Rule]
54
+ #
55
+ # @api public
56
+ def validate(*macros, &block)
57
+ @macros = parse_macros(*macros)
58
+ @block = block if block
59
+ self
60
+ end
61
+
62
+ # Define a validation function for each element of an array
63
+ #
64
+ # The function will be applied only if schema checks passed
65
+ # for a given array item.
66
+ #
67
+ # @example
68
+ # rule(:nums).each do
69
+ # key.failure("must be greater than 0") if value < 0
70
+ # end
71
+ # rule(:nums).each(min: 3)
72
+ # rule(address: :city) do
73
+ # key.failure("oops") if value != 'Munich'
74
+ # end
75
+ #
76
+ # @return [Rule]
77
+ #
78
+ # @api public
79
+ def each(*macros, &block)
80
+ root = keys[0]
81
+ macros = parse_macros(*macros)
82
+ @keys = []
83
+
84
+ @block = proc do
85
+ unless result.base_error?(root) || !values.key?(root)
86
+ values[root].each_with_index do |_, idx|
87
+ path = [*Schema::Path[root].to_a, idx]
88
+
89
+ next if result.error?(path)
90
+
91
+ evaluator = with(macros: macros, keys: [path], &block)
92
+
93
+ failures.concat(evaluator.failures)
94
+ end
95
+ end
96
+ end
97
+
98
+ @block_options = map_keywords(block) if block
99
+
100
+ self
101
+ end
102
+
103
+ # Return a nice string representation
104
+ #
105
+ # @return [String]
106
+ #
107
+ # @api public
108
+ def inspect
109
+ %(#<#{self.class} keys=#{keys.inspect}>)
110
+ end
111
+
112
+ # Parse function arguments into macros structure
113
+ #
114
+ # @return [Array]
115
+ #
116
+ # @api private
117
+ def parse_macros(*args)
118
+ args.each_with_object([]) do |spec, macros|
119
+ case spec
120
+ when Hash
121
+ spec.each { |k, v| macros << [k, Array(v)] }
122
+ else
123
+ macros << Array(spec)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/schema/key'
4
+ require 'dry/schema/key_map'
5
+
6
+ module Dry
7
+ module Schema
8
+ class Path
9
+ # @api private
10
+ def multi_value?
11
+ last.is_a?(Array)
12
+ end
13
+
14
+ # @api private
15
+ def expand
16
+ to_a[0..-2].product(last).map { |spec| self.class[spec] }
17
+ end
18
+ end
19
+
20
+ # @api private
21
+ #
22
+ # TODO: this should be moved to dry-schema at some point
23
+ class Key
24
+ # @api private
25
+ def to_dot_notation
26
+ [name.to_s]
27
+ end
28
+
29
+ # @api private
30
+ class Hash < Key
31
+ # @api private
32
+ def to_dot_notation
33
+ [name].product(members.map(&:to_dot_notation).flatten(1)).map { |e| e.join(DOT) }
34
+ end
35
+ end
36
+ end
37
+
38
+ # @api private
39
+ class KeyMap
40
+ # @api private
41
+ def to_dot_notation
42
+ @to_dot_notation ||= map(&:to_dot_notation).flatten
43
+ end
44
+ end
45
+ end
46
+ end