dry-validation 0.1.0 → 1.8.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 (82) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +969 -1
  3. data/LICENSE +1 -1
  4. data/README.md +19 -286
  5. data/config/errors.yml +4 -35
  6. data/dry-validation.gemspec +38 -22
  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 +173 -0
  11. data/lib/dry/validation/evaluator.rb +233 -0
  12. data/lib/dry/validation/extensions/hints.rb +67 -0
  13. data/lib/dry/validation/extensions/monads.rb +34 -0
  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 +43 -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 +100 -0
  20. data/lib/dry/validation/message_set.rb +97 -0
  21. data/lib/dry/validation/messages/resolver.rb +129 -0
  22. data/lib/dry/validation/result.rb +206 -38
  23. data/lib/dry/validation/rule.rb +116 -106
  24. data/lib/dry/validation/schema_ext.rb +19 -0
  25. data/lib/dry/validation/values.rb +108 -0
  26. data/lib/dry/validation/version.rb +3 -1
  27. data/lib/dry/validation.rb +55 -7
  28. data/lib/dry-validation.rb +3 -1
  29. metadata +80 -106
  30. data/.gitignore +0 -8
  31. data/.rspec +0 -3
  32. data/.rubocop.yml +0 -16
  33. data/.rubocop_todo.yml +0 -7
  34. data/.travis.yml +0 -29
  35. data/Gemfile +0 -11
  36. data/Rakefile +0 -12
  37. data/examples/basic.rb +0 -21
  38. data/examples/nested.rb +0 -30
  39. data/examples/rule_ast.rb +0 -33
  40. data/lib/dry/validation/error.rb +0 -43
  41. data/lib/dry/validation/error_compiler.rb +0 -116
  42. data/lib/dry/validation/messages.rb +0 -71
  43. data/lib/dry/validation/predicate.rb +0 -39
  44. data/lib/dry/validation/predicate_set.rb +0 -22
  45. data/lib/dry/validation/predicates.rb +0 -88
  46. data/lib/dry/validation/rule_compiler.rb +0 -57
  47. data/lib/dry/validation/schema/definition.rb +0 -15
  48. data/lib/dry/validation/schema/key.rb +0 -39
  49. data/lib/dry/validation/schema/rule.rb +0 -28
  50. data/lib/dry/validation/schema/value.rb +0 -31
  51. data/lib/dry/validation/schema.rb +0 -74
  52. data/rakelib/rubocop.rake +0 -18
  53. data/spec/fixtures/errors.yml +0 -4
  54. data/spec/integration/custom_error_messages_spec.rb +0 -35
  55. data/spec/integration/custom_predicates_spec.rb +0 -57
  56. data/spec/integration/validation_spec.rb +0 -118
  57. data/spec/shared/predicates.rb +0 -31
  58. data/spec/spec_helper.rb +0 -18
  59. data/spec/unit/error_compiler_spec.rb +0 -165
  60. data/spec/unit/predicate_spec.rb +0 -37
  61. data/spec/unit/predicates/empty_spec.rb +0 -38
  62. data/spec/unit/predicates/eql_spec.rb +0 -21
  63. data/spec/unit/predicates/exclusion_spec.rb +0 -35
  64. data/spec/unit/predicates/filled_spec.rb +0 -38
  65. data/spec/unit/predicates/format_spec.rb +0 -21
  66. data/spec/unit/predicates/gt_spec.rb +0 -40
  67. data/spec/unit/predicates/gteq_spec.rb +0 -40
  68. data/spec/unit/predicates/inclusion_spec.rb +0 -35
  69. data/spec/unit/predicates/int_spec.rb +0 -34
  70. data/spec/unit/predicates/key_spec.rb +0 -29
  71. data/spec/unit/predicates/lt_spec.rb +0 -40
  72. data/spec/unit/predicates/lteq_spec.rb +0 -40
  73. data/spec/unit/predicates/max_size_spec.rb +0 -49
  74. data/spec/unit/predicates/min_size_spec.rb +0 -49
  75. data/spec/unit/predicates/nil_spec.rb +0 -28
  76. data/spec/unit/predicates/size_spec.rb +0 -49
  77. data/spec/unit/predicates/str_spec.rb +0 -32
  78. data/spec/unit/rule/each_spec.rb +0 -20
  79. data/spec/unit/rule/key_spec.rb +0 -27
  80. data/spec/unit/rule/set_spec.rb +0 -32
  81. data/spec/unit/rule/value_spec.rb +0 -42
  82. data/spec/unit/rule_compiler_spec.rb +0 -86
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+ require "dry/schema/messages"
5
+ require "dry/schema/path"
6
+ require "dry/schema/key_map"
7
+
8
+ require "dry/validation/constants"
9
+ require "dry/validation/macros"
10
+ require "dry/validation/schema_ext"
11
+
12
+ module Dry
13
+ module Validation
14
+ class Contract
15
+ # Contract's class interface
16
+ #
17
+ # @see Contract
18
+ #
19
+ # @api public
20
+ module ClassInterface
21
+ include Macros::Registrar
22
+
23
+ # @api private
24
+ def inherited(klass)
25
+ super
26
+ klass.instance_variable_set("@config", config.dup)
27
+ end
28
+
29
+ # Configuration
30
+ #
31
+ # @example
32
+ # class MyContract < Dry::Validation::Contract
33
+ # config.messages.backend = :i18n
34
+ # end
35
+ #
36
+ # @return [Config]
37
+ #
38
+ # @api public
39
+ def config
40
+ @config ||= Validation::Config.new
41
+ end
42
+
43
+ # Return macros registered for this class
44
+ #
45
+ # @return [Macros::Container]
46
+ #
47
+ # @api public
48
+ def macros
49
+ config.macros
50
+ end
51
+
52
+ # Define a params schema for your contract
53
+ #
54
+ # This type of schema is suitable for HTTP parameters
55
+ #
56
+ # @return [Dry::Schema::Params,NilClass]
57
+ # @see https://dry-rb.org/gems/dry-schema/params/
58
+ #
59
+ # @api public
60
+ def params(*external_schemas, &block)
61
+ define(:Params, external_schemas, &block)
62
+ end
63
+
64
+ # Define a JSON schema for your contract
65
+ #
66
+ # This type of schema is suitable for JSON data
67
+ #
68
+ # @return [Dry::Schema::JSON,NilClass]
69
+ # @see https://dry-rb.org/gems/dry-schema/json/
70
+ #
71
+ # @api public
72
+ def json(*external_schemas, &block)
73
+ define(:JSON, external_schemas, &block)
74
+ end
75
+
76
+ # Define a plain schema for your contract
77
+ #
78
+ # This type of schema does not offer coercion out of the box
79
+ #
80
+ # @return [Dry::Schema::Processor,NilClass]
81
+ # @see https://dry-rb.org/gems/dry-schema/
82
+ #
83
+ # @api public
84
+ def schema(*external_schemas, &block)
85
+ define(:schema, external_schemas, &block)
86
+ end
87
+
88
+ # Define a rule for your contract
89
+ #
90
+ # @example using a symbol
91
+ # rule(:age) do
92
+ # failure('must be at least 18') if values[:age] < 18
93
+ # end
94
+ #
95
+ # @example using a path to a value and a custom predicate
96
+ # rule('address.street') do
97
+ # failure('please provide a valid street address') if valid_street?(values[:street])
98
+ # end
99
+ #
100
+ # @return [Rule]
101
+ #
102
+ # @api public
103
+ def rule(*keys, &block)
104
+ ensure_valid_keys(*keys) if __schema__
105
+
106
+ Rule.new(keys: keys, block: block).tap do |rule|
107
+ rules << rule
108
+ end
109
+ end
110
+
111
+ # A shortcut that can be used to define contracts that won't be reused or inherited
112
+ #
113
+ # @example
114
+ # my_contract = Dry::Validation::Contract.build do
115
+ # params do
116
+ # required(:name).filled(:string)
117
+ # end
118
+ # end
119
+ #
120
+ # my_contract.call(name: "Jane")
121
+ #
122
+ # @return [Contract]
123
+ #
124
+ # @api public
125
+ def build(options = EMPTY_HASH, &block)
126
+ Class.new(self, &block).new(**options)
127
+ end
128
+
129
+ # @api private
130
+ def __schema__
131
+ @__schema__ if defined?(@__schema__)
132
+ end
133
+
134
+ # Return rules defined in this class
135
+ #
136
+ # @return [Array<Rule>]
137
+ #
138
+ # @api private
139
+ def rules
140
+ @rules ||= EMPTY_ARRAY
141
+ .dup
142
+ .concat(superclass.respond_to?(:rules) ? superclass.rules : EMPTY_ARRAY)
143
+ end
144
+
145
+ # Return messages configured for this class
146
+ #
147
+ # @return [Dry::Schema::Messages]
148
+ #
149
+ # @api private
150
+ def messages
151
+ @messages ||= Schema::Messages.setup(config.messages)
152
+ end
153
+
154
+ private
155
+
156
+ # @api private
157
+ def ensure_valid_keys(*keys)
158
+ valid_paths = key_map.to_dot_notation
159
+ key_paths = key_paths(keys)
160
+
161
+ invalid_keys = key_paths.map { |(key, path)|
162
+ unless valid_paths.any? { |vp| vp.include?(path) || vp.include?("#{path}[]") }
163
+ key
164
+ end
165
+ }.compact.uniq
166
+
167
+ return if invalid_keys.empty?
168
+
169
+ raise InvalidKeysError, <<~STR.strip
170
+ #{name}.rule specifies keys that are not defined by the schema: #{invalid_keys.inspect}
171
+ STR
172
+ end
173
+
174
+ # @api private
175
+ def key_paths(keys)
176
+ keys.map { |key|
177
+ case key
178
+ when Hash
179
+ path = Schema::Path[key]
180
+ if path.multi_value?
181
+ *head, tail = Array(path)
182
+ [key].product(
183
+ tail.map { |el| [*head, *el] }.map { |parts| parts.join(DOT) }
184
+ )
185
+ else
186
+ [[key, path.to_a.join(DOT)]]
187
+ end
188
+ when Array
189
+ [[key, Schema::Path[key].to_a.join(DOT)]]
190
+ else
191
+ [[key, key.to_s]]
192
+ end
193
+ }.flatten(1)
194
+ end
195
+
196
+ # @api private
197
+ def key_map
198
+ __schema__.key_map
199
+ end
200
+
201
+ # @api private
202
+ def core_schema_opts
203
+ {parent: superclass&.__schema__, config: config}
204
+ end
205
+
206
+ # @api private
207
+ def define(method_name, external_schemas, &block)
208
+ return __schema__ if external_schemas.empty? && block.nil?
209
+
210
+ unless __schema__.nil?
211
+ raise ::Dry::Validation::DuplicateSchemaError, "Schema has already been defined"
212
+ end
213
+
214
+ schema_opts = core_schema_opts
215
+
216
+ schema_opts.update(parent: external_schemas) if external_schemas.any?
217
+
218
+ case method_name
219
+ when :schema
220
+ @__schema__ = Schema.define(**schema_opts, &block)
221
+ when :Params
222
+ @__schema__ = Schema.Params(**schema_opts, &block)
223
+ when :JSON
224
+ @__schema__ = Schema.JSON(**schema_opts, &block)
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
5
+ require "dry/core/equalizer"
6
+ require "dry/initializer"
7
+ require "dry/schema/path"
8
+
9
+ require "dry/validation/config"
10
+ require "dry/validation/constants"
11
+ require "dry/validation/rule"
12
+ require "dry/validation/evaluator"
13
+ require "dry/validation/messages/resolver"
14
+ require "dry/validation/result"
15
+ require "dry/validation/contract/class_interface"
16
+
17
+ module Dry
18
+ module Validation
19
+ # Contract objects apply rules to input
20
+ #
21
+ # A contract consists of a schema and rules. The schema is applied to the
22
+ # input before rules are applied, this way you can be sure that your rules
23
+ # won't be applied to values that didn't pass schema checks.
24
+ #
25
+ # It's up to you how exactly you're going to separate schema checks from
26
+ # your rules.
27
+ #
28
+ # @example
29
+ # class NewUserContract < Dry::Validation::Contract
30
+ # params do
31
+ # required(:email).filled(:string)
32
+ # required(:age).filled(:integer)
33
+ # optional(:login).maybe(:string, :filled?)
34
+ # optional(:password).maybe(:string, min_size?: 10)
35
+ # optional(:password_confirmation).maybe(:string)
36
+ # end
37
+ #
38
+ # rule(:password) do
39
+ # key.failure('is required') if values[:login] && !values[:password]
40
+ # end
41
+ #
42
+ # rule(:age) do
43
+ # key.failure('must be greater or equal 18') if values[:age] < 18
44
+ # end
45
+ # end
46
+ #
47
+ # new_user_contract = NewUserContract.new
48
+ # new_user_contract.call(email: 'jane@doe.org', age: 21)
49
+ #
50
+ # @api public
51
+ class Contract
52
+ include Dry::Equalizer(:schema, :rules, :messages, inspect: false)
53
+
54
+ extend Dry::Initializer
55
+ extend ClassInterface
56
+
57
+ config.messages.top_namespace = DEFAULT_ERRORS_NAMESPACE
58
+ config.messages.load_paths << DEFAULT_ERRORS_PATH
59
+
60
+ # @!attribute [r] config
61
+ # @return [Config] Contract's configuration object
62
+ # @api public
63
+ option :config, default: -> { self.class.config }
64
+
65
+ # @!attribute [r] macros
66
+ # @return [Macros::Container] Configured macros
67
+ # @see Macros::Container#register
68
+ # @api public
69
+ option :macros, default: -> { config.macros }
70
+
71
+ # @!attribute [r] default_context
72
+ # @return [Hash] Default context for rules
73
+ # @api public
74
+ option :default_context, default: -> { EMPTY_HASH }
75
+
76
+ # @!attribute [r] schema
77
+ # @return [Dry::Schema::Params, Dry::Schema::JSON, Dry::Schema::Processor]
78
+ # @api private
79
+ option :schema, default: -> { self.class.__schema__ || raise(SchemaMissingError, self.class) }
80
+
81
+ # @!attribute [r] rules
82
+ # @return [Hash]
83
+ # @api private
84
+ option :rules, default: -> { self.class.rules }
85
+
86
+ # @!attribute [r] message_resolver
87
+ # @return [Messages::Resolver]
88
+ # @api private
89
+ option :message_resolver, default: -> { Messages::Resolver.new(messages) }
90
+
91
+ # Apply the contract to an input
92
+ #
93
+ # @param [Hash] input The input to validate
94
+ # @param [Hash] context Initial context for rules
95
+ #
96
+ # @return [Result]
97
+ #
98
+ # @api public
99
+ # rubocop: disable Metrics/AbcSize
100
+ def call(input, context = EMPTY_HASH)
101
+ context_map = Concurrent::Map.new.tap do |map|
102
+ default_context.each { |key, value| map[key] = value }
103
+ context.each { |key, value| map[key] = value }
104
+ end
105
+
106
+ Result.new(schema.(input), context_map) do |result|
107
+ rules.each do |rule|
108
+ next if rule.keys.any? { |key| error?(result, key) }
109
+
110
+ rule_result = rule.(self, result)
111
+
112
+ rule_result.failures.each do |failure|
113
+ result.add_error(message_resolver.(**failure))
114
+ end
115
+ end
116
+ end
117
+ end
118
+ # rubocop: enable Metrics/AbcSize
119
+
120
+ # Return a nice string representation
121
+ #
122
+ # @return [String]
123
+ #
124
+ # @api public
125
+ def inspect
126
+ %(#<#{self.class} schema=#{schema.inspect} rules=#{rules.inspect}>)
127
+ end
128
+
129
+ private
130
+
131
+ # @api private
132
+ def error?(result, spec)
133
+ path = Schema::Path[spec]
134
+
135
+ if path.multi_value?
136
+ return path.expand.any? { |nested_path| error?(result, nested_path) }
137
+ end
138
+
139
+ return true if result.schema_error?(path)
140
+
141
+ path
142
+ .to_a[0..-2]
143
+ .any? { |key|
144
+ curr_path = Schema::Path[path.keys[0..path.keys.index(key)]]
145
+
146
+ return false unless result.schema_error?(curr_path)
147
+
148
+ result.errors.any? { |err|
149
+ (other = Schema::Path[err.path]).same_root?(curr_path) && other == curr_path
150
+ }
151
+ }
152
+ end
153
+
154
+ # Get a registered macro
155
+ #
156
+ # @return [Proc,#to_proc]
157
+ #
158
+ # @api private
159
+ def macro(name, *args)
160
+ (macros.key?(name) ? macros[name] : Macros[name]).with(args)
161
+ end
162
+
163
+ # Return configured messages backend
164
+ #
165
+ # @return [Dry::Schema::Messages::YAML, Dry::Schema::Messages::I18n]
166
+ #
167
+ # @api private
168
+ def messages
169
+ self.class.messages
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,233 @@
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.transform_values { _options[_1] }
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
+ # Check if there are any base rule errors
204
+ #
205
+ # @return [Boolean]
206
+ #
207
+ # @api public
208
+ def base_rule_error?
209
+ !base.empty? || result.base_rule_error?
210
+ end
211
+
212
+ # @api private
213
+ def respond_to_missing?(meth, include_private = false)
214
+ super || _contract.respond_to?(meth, true)
215
+ end
216
+
217
+ private
218
+
219
+ # Forward to the underlying contract
220
+ #
221
+ # @api private
222
+ def method_missing(meth, *args, &block)
223
+ # yes, we do want to delegate to private methods too
224
+ if _contract.respond_to?(meth, true)
225
+ _contract.__send__(meth, *args, &block)
226
+ else
227
+ super
228
+ end
229
+ end
230
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
231
+ end
232
+ end
233
+ 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