dry-validation 0.13.3 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (207) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +506 -95
  3. data/LICENSE +1 -1
  4. data/README.md +16 -12
  5. data/config/errors.yml +3 -88
  6. data/dry-validation.gemspec +37 -24
  7. data/lib/dry/validation/config.rb +24 -0
  8. data/lib/dry/validation/constants.rb +43 -0
  9. data/lib/dry/validation/contract/class_interface.rb +230 -0
  10. data/lib/dry/validation/contract.rb +171 -0
  11. data/lib/dry/validation/evaluator.rb +224 -0
  12. data/lib/dry/validation/extensions/hints.rb +67 -0
  13. data/lib/dry/validation/extensions/monads.rb +24 -8
  14. data/lib/dry/validation/extensions/predicates_as_macros.rb +75 -0
  15. data/lib/dry/validation/failures.rb +70 -0
  16. data/lib/dry/validation/function.rb +44 -0
  17. data/lib/dry/validation/macro.rb +38 -0
  18. data/lib/dry/validation/macros.rb +104 -0
  19. data/lib/dry/validation/message.rb +80 -80
  20. data/lib/dry/validation/message_set.rb +80 -105
  21. data/lib/dry/validation/messages/resolver.rb +131 -0
  22. data/lib/dry/validation/result.rb +183 -41
  23. data/lib/dry/validation/rule.rb +135 -0
  24. data/lib/dry/validation/schema_ext.rb +19 -0
  25. data/lib/dry/validation/values.rb +104 -0
  26. data/lib/dry/validation/version.rb +3 -1
  27. data/lib/dry/validation.rb +45 -28
  28. data/lib/dry-validation.rb +3 -1
  29. metadata +46 -344
  30. data/.codeclimate.yml +0 -17
  31. data/.gitignore +0 -9
  32. data/.rspec +0 -3
  33. data/.travis.yml +0 -29
  34. data/CONTRIBUTING.md +0 -31
  35. data/Gemfile +0 -25
  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/struct.rb +0 -32
  56. data/lib/dry/validation/extensions.rb +0 -7
  57. data/lib/dry/validation/input_processor_compiler/json.rb +0 -45
  58. data/lib/dry/validation/input_processor_compiler/params.rb +0 -49
  59. data/lib/dry/validation/input_processor_compiler/sanitizer.rb +0 -47
  60. data/lib/dry/validation/input_processor_compiler.rb +0 -137
  61. data/lib/dry/validation/message_compiler/visitor_opts.rb +0 -37
  62. data/lib/dry/validation/message_compiler.rb +0 -188
  63. data/lib/dry/validation/messages/abstract.rb +0 -119
  64. data/lib/dry/validation/messages/i18n.rb +0 -47
  65. data/lib/dry/validation/messages/namespaced.rb +0 -39
  66. data/lib/dry/validation/messages/yaml.rb +0 -61
  67. data/lib/dry/validation/messages.rb +0 -14
  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/check.rb +0 -37
  71. data/lib/dry/validation/schema/class_interface.rb +0 -190
  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.rb +0 -126
  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/log/.gitkeep +0 -0
  85. data/spec/extensions/monads/result_spec.rb +0 -40
  86. data/spec/extensions/struct/schema_spec.rb +0 -32
  87. data/spec/fixtures/locales/en.yml +0 -8
  88. data/spec/fixtures/locales/pl.yml +0 -22
  89. data/spec/integration/custom_error_messages_spec.rb +0 -54
  90. data/spec/integration/custom_predicates_spec.rb +0 -228
  91. data/spec/integration/hints_spec.rb +0 -170
  92. data/spec/integration/injecting_rules_spec.rb +0 -30
  93. data/spec/integration/json/defining_base_schema_spec.rb +0 -41
  94. data/spec/integration/localized_error_messages_spec.rb +0 -72
  95. data/spec/integration/message_compiler_spec.rb +0 -405
  96. data/spec/integration/messages/i18n_spec.rb +0 -104
  97. data/spec/integration/optional_keys_spec.rb +0 -28
  98. data/spec/integration/params/predicates/array_spec.rb +0 -287
  99. data/spec/integration/params/predicates/empty_spec.rb +0 -263
  100. data/spec/integration/params/predicates/eql_spec.rb +0 -327
  101. data/spec/integration/params/predicates/even_spec.rb +0 -455
  102. data/spec/integration/params/predicates/excluded_from_spec.rb +0 -455
  103. data/spec/integration/params/predicates/excludes_spec.rb +0 -391
  104. data/spec/integration/params/predicates/false_spec.rb +0 -455
  105. data/spec/integration/params/predicates/filled_spec.rb +0 -467
  106. data/spec/integration/params/predicates/format_spec.rb +0 -454
  107. data/spec/integration/params/predicates/gt_spec.rb +0 -519
  108. data/spec/integration/params/predicates/gteq_spec.rb +0 -519
  109. data/spec/integration/params/predicates/included_in_spec.rb +0 -455
  110. data/spec/integration/params/predicates/includes_spec.rb +0 -391
  111. data/spec/integration/params/predicates/key_spec.rb +0 -67
  112. data/spec/integration/params/predicates/lt_spec.rb +0 -519
  113. data/spec/integration/params/predicates/lteq_spec.rb +0 -519
  114. data/spec/integration/params/predicates/max_size_spec.rb +0 -391
  115. data/spec/integration/params/predicates/min_size_spec.rb +0 -391
  116. data/spec/integration/params/predicates/none_spec.rb +0 -265
  117. data/spec/integration/params/predicates/not_eql_spec.rb +0 -327
  118. data/spec/integration/params/predicates/odd_spec.rb +0 -455
  119. data/spec/integration/params/predicates/size/fixed_spec.rb +0 -393
  120. data/spec/integration/params/predicates/size/range_spec.rb +0 -396
  121. data/spec/integration/params/predicates/true_spec.rb +0 -455
  122. data/spec/integration/params/predicates/type_spec.rb +0 -391
  123. data/spec/integration/result_spec.rb +0 -81
  124. data/spec/integration/schema/array_schema_spec.rb +0 -59
  125. data/spec/integration/schema/check_rules_spec.rb +0 -119
  126. data/spec/integration/schema/check_with_nested_el_spec.rb +0 -37
  127. data/spec/integration/schema/check_with_nth_el_spec.rb +0 -25
  128. data/spec/integration/schema/default_settings_spec.rb +0 -11
  129. data/spec/integration/schema/defining_base_schema_spec.rb +0 -41
  130. data/spec/integration/schema/dynamic_predicate_args_spec.rb +0 -43
  131. data/spec/integration/schema/each_with_set_spec.rb +0 -70
  132. data/spec/integration/schema/extending_dsl_spec.rb +0 -27
  133. data/spec/integration/schema/form_spec.rb +0 -236
  134. data/spec/integration/schema/hash_schema_spec.rb +0 -47
  135. data/spec/integration/schema/inheriting_schema_spec.rb +0 -31
  136. data/spec/integration/schema/input_processor_spec.rb +0 -46
  137. data/spec/integration/schema/json/explicit_types_spec.rb +0 -157
  138. data/spec/integration/schema/json_spec.rb +0 -163
  139. data/spec/integration/schema/macros/confirmation_spec.rb +0 -35
  140. data/spec/integration/schema/macros/each_spec.rb +0 -268
  141. data/spec/integration/schema/macros/filled_spec.rb +0 -87
  142. data/spec/integration/schema/macros/input_spec.rb +0 -139
  143. data/spec/integration/schema/macros/maybe_spec.rb +0 -99
  144. data/spec/integration/schema/macros/rule_spec.rb +0 -75
  145. data/spec/integration/schema/macros/value_spec.rb +0 -119
  146. data/spec/integration/schema/macros/when_spec.rb +0 -62
  147. data/spec/integration/schema/nested_schemas_spec.rb +0 -236
  148. data/spec/integration/schema/nested_values_spec.rb +0 -46
  149. data/spec/integration/schema/not_spec.rb +0 -34
  150. data/spec/integration/schema/numbers_spec.rb +0 -19
  151. data/spec/integration/schema/option_with_default_spec.rb +0 -64
  152. data/spec/integration/schema/or_spec.rb +0 -87
  153. data/spec/integration/schema/params/defining_base_schema_spec.rb +0 -41
  154. data/spec/integration/schema/params/explicit_types_spec.rb +0 -195
  155. data/spec/integration/schema/params_spec.rb +0 -234
  156. data/spec/integration/schema/predicate_verification_spec.rb +0 -9
  157. data/spec/integration/schema/predicates/array_spec.rb +0 -295
  158. data/spec/integration/schema/predicates/custom_spec.rb +0 -103
  159. data/spec/integration/schema/predicates/empty_spec.rb +0 -263
  160. data/spec/integration/schema/predicates/eql_spec.rb +0 -327
  161. data/spec/integration/schema/predicates/even_spec.rb +0 -455
  162. data/spec/integration/schema/predicates/excluded_from/array_spec.rb +0 -459
  163. data/spec/integration/schema/predicates/excluded_from/range_spec.rb +0 -459
  164. data/spec/integration/schema/predicates/excludes_spec.rb +0 -391
  165. data/spec/integration/schema/predicates/filled_spec.rb +0 -467
  166. data/spec/integration/schema/predicates/format_spec.rb +0 -455
  167. data/spec/integration/schema/predicates/gt_spec.rb +0 -519
  168. data/spec/integration/schema/predicates/gteq_spec.rb +0 -519
  169. data/spec/integration/schema/predicates/hash_spec.rb +0 -69
  170. data/spec/integration/schema/predicates/included_in/array_spec.rb +0 -459
  171. data/spec/integration/schema/predicates/included_in/range_spec.rb +0 -459
  172. data/spec/integration/schema/predicates/includes_spec.rb +0 -391
  173. data/spec/integration/schema/predicates/key_spec.rb +0 -88
  174. data/spec/integration/schema/predicates/lt_spec.rb +0 -520
  175. data/spec/integration/schema/predicates/lteq_spec.rb +0 -519
  176. data/spec/integration/schema/predicates/max_size_spec.rb +0 -391
  177. data/spec/integration/schema/predicates/min_size_spec.rb +0 -391
  178. data/spec/integration/schema/predicates/none_spec.rb +0 -265
  179. data/spec/integration/schema/predicates/not_eql_spec.rb +0 -391
  180. data/spec/integration/schema/predicates/odd_spec.rb +0 -455
  181. data/spec/integration/schema/predicates/size/fixed_spec.rb +0 -398
  182. data/spec/integration/schema/predicates/size/range_spec.rb +0 -395
  183. data/spec/integration/schema/predicates/type_spec.rb +0 -413
  184. data/spec/integration/schema/reusing_schema_spec.rb +0 -33
  185. data/spec/integration/schema/using_types_spec.rb +0 -135
  186. data/spec/integration/schema/validate_spec.rb +0 -120
  187. data/spec/integration/schema/xor_spec.rb +0 -35
  188. data/spec/integration/schema_builders_spec.rb +0 -17
  189. data/spec/integration/schema_spec.rb +0 -173
  190. data/spec/shared/message_compiler.rb +0 -11
  191. data/spec/shared/predicate_helper.rb +0 -15
  192. data/spec/shared/rule_compiler.rb +0 -8
  193. data/spec/spec_helper.rb +0 -62
  194. data/spec/support/define_struct.rb +0 -25
  195. data/spec/support/matchers.rb +0 -38
  196. data/spec/support/mutant.rb +0 -9
  197. data/spec/support/predicates_integration.rb +0 -7
  198. data/spec/unit/input_processor_compiler/json_spec.rb +0 -283
  199. data/spec/unit/input_processor_compiler/params_spec.rb +0 -328
  200. data/spec/unit/message_compiler/visit_failure_spec.rb +0 -38
  201. data/spec/unit/message_compiler/visit_spec.rb +0 -16
  202. data/spec/unit/message_compiler_spec.rb +0 -7
  203. data/spec/unit/predicate_registry_spec.rb +0 -34
  204. data/spec/unit/schema/key_spec.rb +0 -38
  205. data/spec/unit/schema/rule_spec.rb +0 -42
  206. data/spec/unit/schema/value_spec.rb +0 -131
  207. data/spec/unit/schema_spec.rb +0 -35
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/initializer"
4
+ require "dry/core/deprecations"
5
+
6
+ require "dry/validation/constants"
7
+ require "dry/validation/failures"
8
+
9
+ module Dry
10
+ module Validation
11
+ # Evaluator is the execution context for rules
12
+ #
13
+ # Evaluators expose an API for setting failure messages and forward
14
+ # method calls to the contracts, so that you can use your contract
15
+ # methods within rule blocks
16
+ #
17
+ # @api public
18
+ class Evaluator
19
+ extend Dry::Initializer
20
+ extend Dry::Core::Deprecations[:'dry-validation']
21
+
22
+ deprecate :error?, :schema_error?
23
+
24
+ # @!attribute [r] _contract
25
+ # @return [Contract]
26
+ # @api private
27
+ param :_contract
28
+
29
+ # @!attribute [r] result
30
+ # @return [Result]
31
+ # @api private
32
+ option :result
33
+
34
+ # @!attribute [r] keys
35
+ # @return [Array<String, Symbol, Hash>]
36
+ # @api private
37
+ option :keys
38
+
39
+ # @!attribute [r] macros
40
+ # @return [Array<Symbol>]
41
+ # @api private
42
+ option :macros, optional: true, default: proc { EMPTY_ARRAY.dup }
43
+
44
+ # @!attribute [r] _context
45
+ # @return [Concurrent::Map]
46
+ # @api private
47
+ option :_context
48
+
49
+ # @!attribute [r] path
50
+ # @return [Dry::Schema::Path]
51
+ # @api private
52
+ option :path, default: proc { Dry::Schema::Path[(key = keys.first) ? key : ROOT_PATH] }
53
+
54
+ # @!attribute [r] values
55
+ # @return [Object]
56
+ # @api private
57
+ option :values
58
+
59
+ # @!attribute [r] block_options
60
+ # @return [Hash<Symbol=>Symbol>]
61
+ # @api private
62
+ option :block_options, default: proc { EMPTY_HASH }
63
+
64
+ # @return [Hash]
65
+ attr_reader :_options
66
+
67
+ # Initialize a new evaluator
68
+ #
69
+ # @api private
70
+ def initialize(contract, **options, &block)
71
+ super(contract, **options)
72
+
73
+ @_options = options
74
+
75
+ if block
76
+ exec_opts = block_options.map { |key, value| [key, _options[value]] }.to_h
77
+ instance_exec(**exec_opts, &block)
78
+ end
79
+
80
+ macros.each do |args|
81
+ macro = macro(*args.flatten(1))
82
+ instance_exec(**macro.extract_block_options(_options.merge(macro: macro)), &macro.block)
83
+ end
84
+ end
85
+
86
+ # Get `Failures` object for the default or provided path
87
+ #
88
+ # @param [Symbol,String,Hash,Array<Symbol>] path
89
+ #
90
+ # @return [Failures]
91
+ #
92
+ # @see Failures#failure
93
+ #
94
+ # @api public
95
+ def key(path = self.path)
96
+ (@key ||= EMPTY_HASH.dup)[path] ||= Failures.new(path)
97
+ end
98
+
99
+ # Get `Failures` object for base errors
100
+ #
101
+ # @return [Failures]
102
+ #
103
+ # @see Failures#failure
104
+ #
105
+ # @api public
106
+ def base
107
+ @base ||= Failures.new
108
+ end
109
+
110
+ # Return aggregated failures
111
+ #
112
+ # @return [Array<Hash>]
113
+ #
114
+ # @api private
115
+ def failures
116
+ @failures ||= []
117
+ @failures += @base.opts if defined?(@base)
118
+ @failures.concat(@key.values.flat_map(&:opts)) if defined?(@key)
119
+ @failures
120
+ end
121
+
122
+ # @api private
123
+ def with(new_opts, &block)
124
+ self.class.new(_contract, **_options, **new_opts, &block)
125
+ end
126
+
127
+ # Return default (first) key name
128
+ #
129
+ # @return [Symbol]
130
+ #
131
+ # @api public
132
+ def key_name
133
+ @key_name ||= keys.first
134
+ end
135
+
136
+ # Return the value found under the first specified key
137
+ #
138
+ # This is a convenient method that can be used in all the common cases
139
+ # where a rule depends on just one key and you want a quick access to
140
+ # the value
141
+ #
142
+ # @example
143
+ # rule(:age) do
144
+ # key.failure(:invalid) if value < 18
145
+ # end
146
+ #
147
+ # @return [Object]
148
+ #
149
+ # @api public
150
+ def value
151
+ values[key_name]
152
+ end
153
+
154
+ # Return if the value under the default key is available
155
+ #
156
+ # This is useful when dealing with rules for optional keys
157
+ #
158
+ # @example use the default key name
159
+ # rule(:age) do
160
+ # key.failure(:invalid) if key? && value < 18
161
+ # end
162
+ #
163
+ # @example specify the key name
164
+ # rule(:start_date, :end_date) do
165
+ # if key?(:start_date) && !key?(:end_date)
166
+ # key(:end_date).failure("must provide an end_date with start_date")
167
+ # end
168
+ # end
169
+ #
170
+ # @return [Boolean]
171
+ #
172
+ # @api public
173
+ def key?(name = key_name)
174
+ values.key?(name)
175
+ end
176
+
177
+ # Check if there are any errors on the schema under the provided path
178
+ #
179
+ # @param path [Symbol, String, Array] A Path-compatible spec
180
+ #
181
+ # @return [Boolean]
182
+ #
183
+ # @api public
184
+ def schema_error?(path)
185
+ result.schema_error?(path)
186
+ end
187
+
188
+ # Check if there are any errors on the current rule
189
+ #
190
+ # @param path [Symbol, String, Array] A Path-compatible spec
191
+ #
192
+ # @return [Boolean]
193
+ #
194
+ # @api public
195
+ def rule_error?(path = nil)
196
+ if path.nil?
197
+ !key(self.path).empty?
198
+ else
199
+ result.rule_error?(path)
200
+ end
201
+ end
202
+
203
+ # @api private
204
+ def respond_to_missing?(meth, include_private = false)
205
+ super || _contract.respond_to?(meth, true)
206
+ end
207
+
208
+ private
209
+
210
+ # Forward to the underlying contract
211
+ #
212
+ # @api private
213
+ def method_missing(meth, *args, &block)
214
+ # yes, we do want to delegate to private methods too
215
+ if _contract.respond_to?(meth, true)
216
+ _contract.__send__(meth, *args, &block)
217
+ else
218
+ super
219
+ end
220
+ end
221
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Validation
5
+ # Hints extension
6
+ #
7
+ # @example
8
+ # Dry::Validation.load_extensions(:hints)
9
+ #
10
+ # contract = Dry::Validation::Contract.build do
11
+ # schema do
12
+ # required(:name).filled(:string, min_size?: 2..4)
13
+ # end
14
+ # end
15
+ #
16
+ # contract.call(name: "fo").hints
17
+ # # {:name=>["size must be within 2 - 4"]}
18
+ #
19
+ # contract.call(name: "").messages
20
+ # # {:name=>["must be filled", "size must be within 2 - 4"]}
21
+ #
22
+ # @api public
23
+ module Hints
24
+ # Hints extensions for Result
25
+ #
26
+ # @api public
27
+ module ResultExtensions
28
+ # Return error messages excluding hints
29
+ #
30
+ # @macro errors-options
31
+ # @return [MessageSet]
32
+ #
33
+ # @api public
34
+ def errors(new_options = EMPTY_HASH)
35
+ opts = new_options.merge(hints: false)
36
+ @errors.with(schema_errors(opts), opts)
37
+ end
38
+
39
+ # Return errors and hints
40
+ #
41
+ # @macro errors-options
42
+ #
43
+ # @return [MessageSet]
44
+ #
45
+ # @api public
46
+ def messages(new_options = EMPTY_HASH)
47
+ errors.with(hints(new_options).to_a, options.merge(**new_options))
48
+ end
49
+
50
+ # Return hint messages
51
+ #
52
+ # @macro errors-options
53
+ #
54
+ # @return [MessageSet]
55
+ #
56
+ # @api public
57
+ def hints(new_options = EMPTY_HASH)
58
+ schema_result.hints(new_options)
59
+ end
60
+ end
61
+
62
+ Dry::Schema.load_extensions(:hints)
63
+
64
+ Result.prepend(ResultExtensions)
65
+ end
66
+ end
67
+ end
@@ -1,18 +1,34 @@
1
- require 'dry/monads/result'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads/result"
2
4
 
3
5
  module Dry
4
6
  module Validation
7
+ # Monad extension for contract results
8
+ #
9
+ # @example
10
+ # Dry::Validation.load_extensions(:monads)
11
+ #
12
+ # contract = Dry::Validation::Contract.build do
13
+ # schema do
14
+ # required(:name).filled(:string)
15
+ # end
16
+ # end
17
+ #
18
+ # contract.call(name: nil).to_monad
19
+ #
20
+ # @api public
5
21
  class Result
6
22
  include Dry::Monads::Result::Mixin
7
23
 
8
- def to_monad(options = EMPTY_HASH)
9
- if success?
10
- Success(output)
11
- else
12
- Failure(messages(options))
13
- end
24
+ # Returns a result monad
25
+ #
26
+ # @return [Dry::Monads::Result]
27
+ #
28
+ # @api public
29
+ def to_monad
30
+ success? ? Success(self) : Failure(self)
14
31
  end
15
- alias_method :to_either, :to_monad
16
32
  end
17
33
  end
18
34
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/predicate_registry"
4
+ require "dry/validation/contract"
5
+
6
+ module Dry
7
+ module Validation
8
+ # Predicate registry with additional needed methods.
9
+ class PredicateRegistry < Schema::PredicateRegistry
10
+ # List of predicates to be imported by `:predicates_as_macros`
11
+ # extension.
12
+ #
13
+ # @see Dry::Validation::Contract
14
+ WHITELIST = %i[
15
+ filled? format? gt? gteq? included_in? includes? inclusion? is? lt?
16
+ lteq? max_size? min_size? not_eql? odd? respond_to? size? true?
17
+ uuid_v4?
18
+ ].freeze
19
+
20
+ # @api private
21
+ def arg_names(name)
22
+ arg_list(name).map(&:first)
23
+ end
24
+
25
+ # @api private
26
+ def call(name, args)
27
+ self[name].(*args)
28
+ end
29
+
30
+ # @api private
31
+ def message_opts(name, arg_values)
32
+ arg_names(name).zip(arg_values).to_h
33
+ end
34
+ end
35
+
36
+ # Extension to use dry-logic predicates as macros.
37
+ #
38
+ # @see Dry::Validation::PredicateRegistry::WHITELIST Available predicates
39
+ #
40
+ # @example
41
+ # Dry::Validation.load_extensions(:predicates_as_macros)
42
+ #
43
+ # class ApplicationContract < Dry::Validation::Contract
44
+ # import_predicates_as_macros
45
+ # end
46
+ #
47
+ # class AgeContract < ApplicationContract
48
+ # schema do
49
+ # required(:age).filled(:integer)
50
+ # end
51
+ #
52
+ # rule(:age).validate(gteq?: 18)
53
+ # end
54
+ #
55
+ # AgeContract.new.(age: 17).errors.first.text
56
+ # # => 'must be greater than or equal to 18'
57
+ #
58
+ # @api public
59
+ class Contract
60
+ # Make macros available for self and its descendants.
61
+ def self.import_predicates_as_macros
62
+ registry = PredicateRegistry.new
63
+
64
+ PredicateRegistry::WHITELIST.each do |name|
65
+ register_macro(name) do |macro:|
66
+ predicate_args = [*macro.args, value]
67
+ message_opts = registry.message_opts(name, predicate_args)
68
+
69
+ key.failure(name, message_opts) unless registry.(name, predicate_args)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/path"
4
+ require "dry/validation/constants"
5
+
6
+ module Dry
7
+ module Validation
8
+ # Failure accumulator object
9
+ #
10
+ # @api public
11
+ class Failures
12
+ # The path for messages accumulated by failures object
13
+ #
14
+ # @return [Dry::Schema::Path]
15
+ #
16
+ # @api private
17
+ attr_reader :path
18
+
19
+ # Options for messages
20
+ #
21
+ # These options are used by MessageResolver
22
+ #
23
+ # @return [Hash]
24
+ #
25
+ # @api private
26
+ attr_reader :opts
27
+
28
+ # @api private
29
+ def initialize(path = ROOT_PATH)
30
+ @path = Dry::Schema::Path[path]
31
+ @opts = EMPTY_ARRAY.dup
32
+ end
33
+
34
+ # Set failure
35
+ #
36
+ # @overload failure(message)
37
+ # Set message text explicitly
38
+ # @param message [String] The message text
39
+ # @example
40
+ # failure('this failed')
41
+ #
42
+ # @overload failure(id)
43
+ # Use message identifier (needs localized messages setup)
44
+ # @param id [Symbol] The message id
45
+ # @example
46
+ # failure(:taken)
47
+ #
48
+ # @overload failure(meta_hash)
49
+ # Use meta_hash[:text] as a message (either explicitely or as an identifier),
50
+ # setting the rest of the hash as error meta attribute
51
+ # @param meta [Hash] The hash containing the message as value for the :text key
52
+ # @example
53
+ # failure({text: :invalid, key: value})
54
+ #
55
+ # @see Evaluator#key
56
+ # @see Evaluator#base
57
+ #
58
+ # @api public
59
+ def failure(message, tokens = EMPTY_HASH)
60
+ opts << {message: message, tokens: tokens, path: path}
61
+ self
62
+ end
63
+
64
+ # @api private
65
+ def empty?
66
+ opts.empty?
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/initializer"
4
+ require "dry/validation/constants"
5
+
6
+ module Dry
7
+ module Validation
8
+ # Abstract class for handling rule blocks
9
+ #
10
+ # @see Rule
11
+ # @see Macro
12
+ #
13
+ # @api private
14
+ class Function
15
+ extend Dry::Initializer
16
+
17
+ # @!attribute [r] block
18
+ # @return [Proc]
19
+ # @api private
20
+ option :block
21
+
22
+ # @!attribute [r] block_options
23
+ # @return [Hash]
24
+ # @api private
25
+ option :block_options, default: -> { block ? map_keywords(block) : EMPTY_HASH }
26
+
27
+ private
28
+
29
+ # Extract options for the block kwargs
30
+ #
31
+ # @param [Proc] block Callable
32
+ # @return Hash
33
+ #
34
+ # @api private
35
+ def map_keywords(block)
36
+ block
37
+ .parameters
38
+ .select { |arg,| arg.equal?(:keyreq) }
39
+ .map { |_, name| [name, BLOCK_OPTIONS_MAPPINGS[name]] }
40
+ .to_h
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/validation/constants"
4
+ require "dry/validation/function"
5
+
6
+ module Dry
7
+ module Validation
8
+ # A wrapper for macro validation blocks
9
+ #
10
+ # @api public
11
+ class Macro < Function
12
+ # @!attribute [r] name
13
+ # @return [Symbol]
14
+ # @api public
15
+ param :name
16
+
17
+ # @!attribute [r] args
18
+ # @return [Array]
19
+ # @api public
20
+ option :args
21
+
22
+ # @!attribute [r] block
23
+ # @return [Proc]
24
+ # @api private
25
+ option :block
26
+
27
+ # @api private
28
+ def with(args)
29
+ self.class.new(name, args: args, block: block)
30
+ end
31
+
32
+ # @api private
33
+ def extract_block_options(options)
34
+ block_options.map { |key, value| [key, options[value]] }.to_h
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/container"
4
+ require "dry/validation/macro"
5
+
6
+ module Dry
7
+ module Validation
8
+ # API for registering and accessing Rule macros
9
+ #
10
+ # @api public
11
+ module Macros
12
+ module Registrar
13
+ # Register a macro
14
+ #
15
+ # @example register a global macro
16
+ # Dry::Validation.register_macro(:even_numbers) do
17
+ # key.failure('all numbers must be even') unless values[key_name].all?(&:even?)
18
+ # end
19
+ #
20
+ # @example register a contract macro
21
+ # class MyContract < Dry::Validation::Contract
22
+ # register_macro(:even_numbers) do
23
+ # key.failure('all numbers must be even') unless values[key_name].all?(&:even?)
24
+ # end
25
+ # end
26
+ #
27
+ # @param [Symbol] name The name of the macro
28
+ # @param [Array] args Optional default positional arguments for the macro
29
+ #
30
+ # @return [self]
31
+ #
32
+ # @see Macro
33
+ #
34
+ # @api public
35
+ def register_macro(name, *args, &block)
36
+ macros.register(name, *args, &block)
37
+ self
38
+ end
39
+ end
40
+
41
+ # Registry for macros
42
+ #
43
+ # @api public
44
+ class Container
45
+ include Dry::Container::Mixin
46
+
47
+ # Register a new macro
48
+ #
49
+ # @example in a contract class
50
+ # class MyContract < Dry::Validation::Contract
51
+ # register_macro(:even_numbers) do
52
+ # key.failure('all numbers must be even') unless values[key_name].all?(&:even?)
53
+ # end
54
+ # end
55
+ #
56
+ # @param [Symbol] name The name of the macro
57
+ #
58
+ # @return [self]
59
+ #
60
+ # @api public
61
+ def register(name, *args, &block)
62
+ macro = Macro.new(name, args: args, block: block)
63
+ super(name, macro, call: false, &nil)
64
+ self
65
+ end
66
+ end
67
+
68
+ # Return a registered macro
69
+ #
70
+ # @param [Symbol] name The name of the macro
71
+ #
72
+ # @return [Proc]
73
+ #
74
+ # @api public
75
+ def self.[](name)
76
+ container[name]
77
+ end
78
+
79
+ # Register a global macro
80
+ #
81
+ # @see Container#register
82
+ #
83
+ # @return [Macros]
84
+ #
85
+ # @api public
86
+ def self.register(name, *args, &block)
87
+ container.register(name, *args, &block)
88
+ self
89
+ end
90
+
91
+ # @api private
92
+ def self.container
93
+ @container ||= Container.new
94
+ end
95
+ end
96
+
97
+ # Acceptance macro
98
+ #
99
+ # @api public
100
+ Macros.register(:acceptance) do
101
+ key.failure(:acceptance, key: key_name) unless values[key_name].equal?(true)
102
+ end
103
+ end
104
+ end