dry-validation 0.12.0 → 1.6.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 (204) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +696 -239
  3. data/LICENSE +1 -1
  4. data/README.md +15 -12
  5. data/config/errors.yml +3 -88
  6. data/dry-validation.gemspec +31 -17
  7. data/lib/dry-validation.rb +3 -1
  8. data/lib/dry/validation.rb +45 -28
  9. data/lib/dry/validation/config.rb +24 -0
  10. data/lib/dry/validation/constants.rb +43 -0
  11. data/lib/dry/validation/contract.rb +171 -0
  12. data/lib/dry/validation/contract/class_interface.rb +230 -0
  13. data/lib/dry/validation/evaluator.rb +224 -0
  14. data/lib/dry/validation/extensions/hints.rb +67 -0
  15. data/lib/dry/validation/extensions/monads.rb +24 -8
  16. data/lib/dry/validation/extensions/predicates_as_macros.rb +75 -0
  17. data/lib/dry/validation/failures.rb +70 -0
  18. data/lib/dry/validation/function.rb +44 -0
  19. data/lib/dry/validation/macro.rb +38 -0
  20. data/lib/dry/validation/macros.rb +104 -0
  21. data/lib/dry/validation/message.rb +80 -80
  22. data/lib/dry/validation/message_set.rb +80 -105
  23. data/lib/dry/validation/messages/resolver.rb +131 -0
  24. data/lib/dry/validation/result.rb +183 -41
  25. data/lib/dry/validation/rule.rb +135 -0
  26. data/lib/dry/validation/schema_ext.rb +19 -0
  27. data/lib/dry/validation/values.rb +104 -0
  28. data/lib/dry/validation/version.rb +3 -1
  29. metadata +47 -336
  30. data/.codeclimate.yml +0 -17
  31. data/.gitignore +0 -9
  32. data/.rspec +0 -3
  33. data/.travis.yml +0 -26
  34. data/CONTRIBUTING.md +0 -31
  35. data/Gemfile +0 -33
  36. data/Rakefile +0 -22
  37. data/benchmarks/benchmark_form_invalid.rb +0 -64
  38. data/benchmarks/benchmark_form_valid.rb +0 -64
  39. data/benchmarks/benchmark_schema_invalid_huge.rb +0 -52
  40. data/benchmarks/profile_schema_call_invalid.rb +0 -20
  41. data/benchmarks/profile_schema_call_valid.rb +0 -20
  42. data/benchmarks/profile_schema_definition.rb +0 -14
  43. data/benchmarks/profile_schema_huge_invalid.rb +0 -30
  44. data/benchmarks/profile_schema_messages_invalid.rb +0 -20
  45. data/benchmarks/suite.rb +0 -5
  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 -135
  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 -192
  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 -112
  65. data/lib/dry/validation/messages/i18n.rb +0 -35
  66. data/lib/dry/validation/messages/namespaced.rb +0 -32
  67. data/lib/dry/validation/messages/yaml.rb +0 -54
  68. data/lib/dry/validation/predicate_registry.rb +0 -115
  69. data/lib/dry/validation/schema.rb +0 -126
  70. data/lib/dry/validation/schema/check.rb +0 -37
  71. data/lib/dry/validation/schema/class_interface.rb +0 -189
  72. data/lib/dry/validation/schema/deprecated.rb +0 -30
  73. data/lib/dry/validation/schema/dsl.rb +0 -118
  74. data/lib/dry/validation/schema/form.rb +0 -9
  75. data/lib/dry/validation/schema/json.rb +0 -21
  76. data/lib/dry/validation/schema/key.rb +0 -71
  77. data/lib/dry/validation/schema/params.rb +0 -22
  78. data/lib/dry/validation/schema/rule.rb +0 -202
  79. data/lib/dry/validation/schema/value.rb +0 -211
  80. data/lib/dry/validation/schema_compiler.rb +0 -81
  81. data/lib/dry/validation/type_specs.rb +0 -70
  82. data/spec/extensions/monads/result_spec.rb +0 -40
  83. data/spec/extensions/struct/schema_spec.rb +0 -32
  84. data/spec/fixtures/locales/en.yml +0 -6
  85. data/spec/fixtures/locales/pl.yml +0 -18
  86. data/spec/integration/custom_error_messages_spec.rb +0 -48
  87. data/spec/integration/custom_predicates_spec.rb +0 -228
  88. data/spec/integration/hints_spec.rb +0 -170
  89. data/spec/integration/injecting_rules_spec.rb +0 -30
  90. data/spec/integration/json/defining_base_schema_spec.rb +0 -41
  91. data/spec/integration/localized_error_messages_spec.rb +0 -72
  92. data/spec/integration/message_compiler_spec.rb +0 -405
  93. data/spec/integration/messages/i18n_spec.rb +0 -94
  94. data/spec/integration/optional_keys_spec.rb +0 -28
  95. data/spec/integration/params/predicates/array_spec.rb +0 -287
  96. data/spec/integration/params/predicates/empty_spec.rb +0 -263
  97. data/spec/integration/params/predicates/eql_spec.rb +0 -327
  98. data/spec/integration/params/predicates/even_spec.rb +0 -455
  99. data/spec/integration/params/predicates/excluded_from_spec.rb +0 -455
  100. data/spec/integration/params/predicates/excludes_spec.rb +0 -391
  101. data/spec/integration/params/predicates/false_spec.rb +0 -455
  102. data/spec/integration/params/predicates/filled_spec.rb +0 -467
  103. data/spec/integration/params/predicates/format_spec.rb +0 -454
  104. data/spec/integration/params/predicates/gt_spec.rb +0 -519
  105. data/spec/integration/params/predicates/gteq_spec.rb +0 -519
  106. data/spec/integration/params/predicates/included_in_spec.rb +0 -455
  107. data/spec/integration/params/predicates/includes_spec.rb +0 -391
  108. data/spec/integration/params/predicates/key_spec.rb +0 -67
  109. data/spec/integration/params/predicates/lt_spec.rb +0 -519
  110. data/spec/integration/params/predicates/lteq_spec.rb +0 -519
  111. data/spec/integration/params/predicates/max_size_spec.rb +0 -391
  112. data/spec/integration/params/predicates/min_size_spec.rb +0 -391
  113. data/spec/integration/params/predicates/none_spec.rb +0 -265
  114. data/spec/integration/params/predicates/not_eql_spec.rb +0 -327
  115. data/spec/integration/params/predicates/odd_spec.rb +0 -455
  116. data/spec/integration/params/predicates/size/fixed_spec.rb +0 -393
  117. data/spec/integration/params/predicates/size/range_spec.rb +0 -396
  118. data/spec/integration/params/predicates/true_spec.rb +0 -455
  119. data/spec/integration/params/predicates/type_spec.rb +0 -391
  120. data/spec/integration/result_spec.rb +0 -81
  121. data/spec/integration/schema/array_schema_spec.rb +0 -59
  122. data/spec/integration/schema/check_rules_spec.rb +0 -119
  123. data/spec/integration/schema/check_with_nested_el_spec.rb +0 -37
  124. data/spec/integration/schema/check_with_nth_el_spec.rb +0 -25
  125. data/spec/integration/schema/default_settings_spec.rb +0 -11
  126. data/spec/integration/schema/defining_base_schema_spec.rb +0 -41
  127. data/spec/integration/schema/dynamic_predicate_args_spec.rb +0 -43
  128. data/spec/integration/schema/each_with_set_spec.rb +0 -70
  129. data/spec/integration/schema/extending_dsl_spec.rb +0 -27
  130. data/spec/integration/schema/form_spec.rb +0 -236
  131. data/spec/integration/schema/hash_schema_spec.rb +0 -47
  132. data/spec/integration/schema/inheriting_schema_spec.rb +0 -31
  133. data/spec/integration/schema/input_processor_spec.rb +0 -46
  134. data/spec/integration/schema/json/explicit_types_spec.rb +0 -157
  135. data/spec/integration/schema/json_spec.rb +0 -163
  136. data/spec/integration/schema/macros/confirmation_spec.rb +0 -35
  137. data/spec/integration/schema/macros/each_spec.rb +0 -268
  138. data/spec/integration/schema/macros/filled_spec.rb +0 -87
  139. data/spec/integration/schema/macros/input_spec.rb +0 -139
  140. data/spec/integration/schema/macros/maybe_spec.rb +0 -99
  141. data/spec/integration/schema/macros/rule_spec.rb +0 -75
  142. data/spec/integration/schema/macros/value_spec.rb +0 -119
  143. data/spec/integration/schema/macros/when_spec.rb +0 -62
  144. data/spec/integration/schema/nested_schemas_spec.rb +0 -236
  145. data/spec/integration/schema/nested_values_spec.rb +0 -46
  146. data/spec/integration/schema/not_spec.rb +0 -34
  147. data/spec/integration/schema/numbers_spec.rb +0 -19
  148. data/spec/integration/schema/option_with_default_spec.rb +0 -64
  149. data/spec/integration/schema/or_spec.rb +0 -87
  150. data/spec/integration/schema/params/defining_base_schema_spec.rb +0 -41
  151. data/spec/integration/schema/params/explicit_types_spec.rb +0 -195
  152. data/spec/integration/schema/params_spec.rb +0 -234
  153. data/spec/integration/schema/predicate_verification_spec.rb +0 -9
  154. data/spec/integration/schema/predicates/array_spec.rb +0 -295
  155. data/spec/integration/schema/predicates/custom_spec.rb +0 -103
  156. data/spec/integration/schema/predicates/empty_spec.rb +0 -263
  157. data/spec/integration/schema/predicates/eql_spec.rb +0 -327
  158. data/spec/integration/schema/predicates/even_spec.rb +0 -455
  159. data/spec/integration/schema/predicates/excluded_from/array_spec.rb +0 -459
  160. data/spec/integration/schema/predicates/excluded_from/range_spec.rb +0 -459
  161. data/spec/integration/schema/predicates/excludes_spec.rb +0 -391
  162. data/spec/integration/schema/predicates/filled_spec.rb +0 -467
  163. data/spec/integration/schema/predicates/format_spec.rb +0 -455
  164. data/spec/integration/schema/predicates/gt_spec.rb +0 -519
  165. data/spec/integration/schema/predicates/gteq_spec.rb +0 -519
  166. data/spec/integration/schema/predicates/hash_spec.rb +0 -69
  167. data/spec/integration/schema/predicates/included_in/array_spec.rb +0 -459
  168. data/spec/integration/schema/predicates/included_in/range_spec.rb +0 -459
  169. data/spec/integration/schema/predicates/includes_spec.rb +0 -391
  170. data/spec/integration/schema/predicates/key_spec.rb +0 -88
  171. data/spec/integration/schema/predicates/lt_spec.rb +0 -520
  172. data/spec/integration/schema/predicates/lteq_spec.rb +0 -519
  173. data/spec/integration/schema/predicates/max_size_spec.rb +0 -391
  174. data/spec/integration/schema/predicates/min_size_spec.rb +0 -391
  175. data/spec/integration/schema/predicates/none_spec.rb +0 -265
  176. data/spec/integration/schema/predicates/not_eql_spec.rb +0 -391
  177. data/spec/integration/schema/predicates/odd_spec.rb +0 -455
  178. data/spec/integration/schema/predicates/size/fixed_spec.rb +0 -398
  179. data/spec/integration/schema/predicates/size/range_spec.rb +0 -395
  180. data/spec/integration/schema/predicates/type_spec.rb +0 -413
  181. data/spec/integration/schema/reusing_schema_spec.rb +0 -33
  182. data/spec/integration/schema/using_types_spec.rb +0 -135
  183. data/spec/integration/schema/validate_spec.rb +0 -120
  184. data/spec/integration/schema/xor_spec.rb +0 -35
  185. data/spec/integration/schema_builders_spec.rb +0 -17
  186. data/spec/integration/schema_spec.rb +0 -173
  187. data/spec/shared/message_compiler.rb +0 -11
  188. data/spec/shared/predicate_helper.rb +0 -15
  189. data/spec/shared/rule_compiler.rb +0 -8
  190. data/spec/spec_helper.rb +0 -58
  191. data/spec/support/define_struct.rb +0 -25
  192. data/spec/support/matchers.rb +0 -38
  193. data/spec/support/mutant.rb +0 -9
  194. data/spec/support/predicates_integration.rb +0 -7
  195. data/spec/unit/input_processor_compiler/json_spec.rb +0 -283
  196. data/spec/unit/input_processor_compiler/params_spec.rb +0 -328
  197. data/spec/unit/message_compiler/visit_failure_spec.rb +0 -38
  198. data/spec/unit/message_compiler/visit_spec.rb +0 -16
  199. data/spec/unit/message_compiler_spec.rb +0 -7
  200. data/spec/unit/predicate_registry_spec.rb +0 -34
  201. data/spec/unit/schema/key_spec.rb +0 -38
  202. data/spec/unit/schema/rule_spec.rb +0 -42
  203. data/spec/unit/schema/value_spec.rb +0 -131
  204. data/spec/unit/schema_spec.rb +0 -35
@@ -1,82 +1,224 @@
1
- require 'dry/equalizer'
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+ require "dry/equalizer"
5
+
6
+ require "dry/validation/constants"
7
+ require "dry/validation/message_set"
8
+ require "dry/validation/values"
2
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 public
105
+ def error?(key)
106
+ errors.any? { |msg| Schema::Path[msg.path].include?(Schema::Path[key]) }
107
+ end
108
+
109
+ # Check if the base schema (without rules) includes an error for the provided key
110
+ #
111
+ # @api private
112
+ def schema_error?(key)
113
+ schema_result.error?(key)
114
+ end
115
+
116
+ # Check if the rules includes an error for the provided key
117
+ #
118
+ # @api private
119
+ def rule_error?(key)
120
+ !schema_error?(key) && error?(key)
42
121
  end
43
122
 
44
- def errors(options = EMPTY_HASH)
45
- message_set(options.merge(hints: false)).dump
123
+ # Check if there's any error for the provided key
124
+ #
125
+ # This does not consider errors from the nested values
126
+ #
127
+ # @api private
128
+ def base_error?(key)
129
+ schema_result.errors.any? { |error|
130
+ key_path = Schema::Path[key]
131
+ err_path = Schema::Path[error.path]
132
+
133
+ next unless key_path.same_root?(err_path)
134
+
135
+ key_path == err_path
136
+ }
46
137
  end
47
138
 
48
- def hints(options = EMPTY_HASH)
49
- message_set(options.merge(failures: false)).dump
139
+ # Add a new error for the provided key
140
+ #
141
+ # @api private
142
+ def add_error(error)
143
+ @errors.add(error)
144
+ self
50
145
  end
51
146
 
52
- def message_set(options = EMPTY_HASH)
53
- message_compiler.with(options).(result_ast)
147
+ # Read a value under provided key
148
+ #
149
+ # @param [Symbol] key
150
+ #
151
+ # @return [Object]
152
+ #
153
+ # @api public
154
+ def [](key)
155
+ values[key]
54
156
  end
55
157
 
56
- def to_ast
57
- if name
58
- [type, [name, [:set, result_ast]]]
158
+ # Check if a key was set
159
+ #
160
+ # @param [Symbol] key
161
+ #
162
+ # @return [Bool]
163
+ #
164
+ # @api public
165
+ def key?(key)
166
+ values.key?(key)
167
+ end
168
+
169
+ # Coerce to a hash
170
+ #
171
+ # @api public
172
+ def to_h
173
+ values.to_h
174
+ end
175
+
176
+ # Return a string representation
177
+ #
178
+ # @api public
179
+ def inspect
180
+ if context.empty?
181
+ "#<#{self.class}#{to_h} errors=#{errors.to_h}>"
59
182
  else
60
- ast
183
+ "#<#{self.class}#{to_h} errors=#{errors.to_h} context=#{context.each.to_h}>"
61
184
  end
62
185
  end
63
186
 
64
- def ast(*)
65
- [:set, result_ast]
187
+ # Freeze result and its error set
188
+ #
189
+ # @api private
190
+ def freeze
191
+ values.freeze
192
+ errors.freeze
193
+ super
66
194
  end
67
195
 
68
- def name
69
- Array(path).last
196
+ if RUBY_VERSION >= "2.7"
197
+ # Pattern matching
198
+ #
199
+ # @api private
200
+ def deconstruct_keys(keys)
201
+ values.deconstruct_keys(keys)
202
+ end
203
+
204
+ # Pattern matching
205
+ #
206
+ # @api private
207
+ def deconstruct
208
+ [values, context.each.to_h]
209
+ end
70
210
  end
71
211
 
72
212
  private
73
213
 
74
- def type
75
- success? ? :success : :failure
214
+ # @api private
215
+ def initialize_errors(options = self.options)
216
+ MessageSet.new(schema_errors(options), options)
76
217
  end
77
218
 
78
- def result_ast
79
- @result_ast ||= results.map(&:to_ast)
219
+ # @api private
220
+ def schema_errors(options)
221
+ schema_result.message_set(options).to_a
80
222
  end
81
223
  end
82
224
  end
@@ -0,0 +1,135 @@
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 |index:|
69
+ # key([:number, index]).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.schema_error?(path)
90
+
91
+ evaluator = with(macros: macros, keys: [path], index: idx, &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
+ add_macro_from_hash(macros, spec)
122
+ else
123
+ macros << Array(spec)
124
+ end
125
+ end
126
+ end
127
+
128
+ def add_macro_from_hash(macros, spec)
129
+ spec.each do |k, v|
130
+ macros << [k, v.is_a?(Array) ? v : [v]]
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/path"
4
+
5
+ module Dry
6
+ module Schema
7
+ class Path
8
+ # @api private
9
+ def multi_value?
10
+ last.is_a?(Array)
11
+ end
12
+
13
+ # @api private
14
+ def expand
15
+ to_a[0..-2].product(last).map { |spec| self.class[spec] }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/equalizer"
4
+ require "dry/schema/path"
5
+ require "dry/validation/constants"
6
+
7
+ module Dry
8
+ module Validation
9
+ # A convenient wrapper for data processed by schemas
10
+ #
11
+ # Values are available within the rule blocks. They act as hash-like
12
+ # objects and expose a convenient API for accessing data.
13
+ #
14
+ # @api public
15
+ class Values
16
+ include Enumerable
17
+ include Dry::Equalizer(:data)
18
+
19
+ # Schema's result output
20
+ #
21
+ # @return [Hash]
22
+ #
23
+ # @api private
24
+ attr_reader :data
25
+
26
+ # @api private
27
+ def initialize(data)
28
+ @data = data
29
+ end
30
+
31
+ # Read from the provided key
32
+ #
33
+ # @example
34
+ # rule(:age) do
35
+ # key.failure('must be > 18') if values[:age] <= 18
36
+ # end
37
+ #
38
+ # @param args [Symbol, String, Hash, Array<Symbol>] If given as a single
39
+ # Symbol, String, Array or Hash, build a key array using
40
+ # {Dry::Schema::Path} digging for data. If given as positional
41
+ # arguments, use these with Hash#dig on the data directly.
42
+ #
43
+ # @return [Object]
44
+ #
45
+ # @api public
46
+ def [](*args)
47
+ return data.dig(*args) if args.size > 1
48
+
49
+ case (key = args[0])
50
+ when Symbol, String, Array, Hash
51
+ keys = Schema::Path[key].to_a
52
+
53
+ return data.dig(*keys) unless keys.last.is_a?(Array)
54
+
55
+ last = keys.pop
56
+ vals = self.class.new(data.dig(*keys))
57
+ vals.fetch_values(*last) { nil }
58
+ else
59
+ raise ArgumentError, "+key+ must be a valid path specification"
60
+ end
61
+ end
62
+
63
+ # @api public
64
+ def key?(key, hash = data)
65
+ return hash.key?(key) if key.is_a?(Symbol)
66
+
67
+ Schema::Path[key].reduce(hash) do |a, e|
68
+ if e.is_a?(Array)
69
+ result = e.all? { |k| key?(k, a) }
70
+ return result
71
+ elsif e.is_a?(Symbol) && a.is_a?(Array)
72
+ return false
73
+ elsif a.nil?
74
+ return false
75
+ elsif a.is_a?(String)
76
+ return false
77
+ else
78
+ return false unless a.is_a?(Array) ? (e >= 0 && e < a.size) : a.key?(e)
79
+ end
80
+ a[e]
81
+ end
82
+
83
+ true
84
+ end
85
+
86
+ # @api private
87
+ def respond_to_missing?(meth, include_private = false)
88
+ super || data.respond_to?(meth, include_private)
89
+ end
90
+
91
+ private
92
+
93
+ # @api private
94
+ def method_missing(meth, *args, &block)
95
+ if data.respond_to?(meth)
96
+ data.public_send(meth, *args, &block)
97
+ else
98
+ super
99
+ end
100
+ end
101
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
102
+ end
103
+ end
104
+ end