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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +969 -1
- data/LICENSE +1 -1
- data/README.md +19 -286
- data/config/errors.yml +4 -35
- data/dry-validation.gemspec +38 -22
- data/lib/dry/validation/config.rb +24 -0
- data/lib/dry/validation/constants.rb +43 -0
- data/lib/dry/validation/contract/class_interface.rb +230 -0
- data/lib/dry/validation/contract.rb +173 -0
- data/lib/dry/validation/evaluator.rb +233 -0
- data/lib/dry/validation/extensions/hints.rb +67 -0
- data/lib/dry/validation/extensions/monads.rb +34 -0
- data/lib/dry/validation/extensions/predicates_as_macros.rb +75 -0
- data/lib/dry/validation/failures.rb +70 -0
- data/lib/dry/validation/function.rb +43 -0
- data/lib/dry/validation/macro.rb +38 -0
- data/lib/dry/validation/macros.rb +104 -0
- data/lib/dry/validation/message.rb +100 -0
- data/lib/dry/validation/message_set.rb +97 -0
- data/lib/dry/validation/messages/resolver.rb +129 -0
- data/lib/dry/validation/result.rb +206 -38
- data/lib/dry/validation/rule.rb +116 -106
- data/lib/dry/validation/schema_ext.rb +19 -0
- data/lib/dry/validation/values.rb +108 -0
- data/lib/dry/validation/version.rb +3 -1
- data/lib/dry/validation.rb +55 -7
- data/lib/dry-validation.rb +3 -1
- metadata +80 -106
- data/.gitignore +0 -8
- data/.rspec +0 -3
- data/.rubocop.yml +0 -16
- data/.rubocop_todo.yml +0 -7
- data/.travis.yml +0 -29
- data/Gemfile +0 -11
- data/Rakefile +0 -12
- data/examples/basic.rb +0 -21
- data/examples/nested.rb +0 -30
- data/examples/rule_ast.rb +0 -33
- data/lib/dry/validation/error.rb +0 -43
- data/lib/dry/validation/error_compiler.rb +0 -116
- data/lib/dry/validation/messages.rb +0 -71
- data/lib/dry/validation/predicate.rb +0 -39
- data/lib/dry/validation/predicate_set.rb +0 -22
- data/lib/dry/validation/predicates.rb +0 -88
- data/lib/dry/validation/rule_compiler.rb +0 -57
- data/lib/dry/validation/schema/definition.rb +0 -15
- data/lib/dry/validation/schema/key.rb +0 -39
- data/lib/dry/validation/schema/rule.rb +0 -28
- data/lib/dry/validation/schema/value.rb +0 -31
- data/lib/dry/validation/schema.rb +0 -74
- data/rakelib/rubocop.rake +0 -18
- data/spec/fixtures/errors.yml +0 -4
- data/spec/integration/custom_error_messages_spec.rb +0 -35
- data/spec/integration/custom_predicates_spec.rb +0 -57
- data/spec/integration/validation_spec.rb +0 -118
- data/spec/shared/predicates.rb +0 -31
- data/spec/spec_helper.rb +0 -18
- data/spec/unit/error_compiler_spec.rb +0 -165
- data/spec/unit/predicate_spec.rb +0 -37
- data/spec/unit/predicates/empty_spec.rb +0 -38
- data/spec/unit/predicates/eql_spec.rb +0 -21
- data/spec/unit/predicates/exclusion_spec.rb +0 -35
- data/spec/unit/predicates/filled_spec.rb +0 -38
- data/spec/unit/predicates/format_spec.rb +0 -21
- data/spec/unit/predicates/gt_spec.rb +0 -40
- data/spec/unit/predicates/gteq_spec.rb +0 -40
- data/spec/unit/predicates/inclusion_spec.rb +0 -35
- data/spec/unit/predicates/int_spec.rb +0 -34
- data/spec/unit/predicates/key_spec.rb +0 -29
- data/spec/unit/predicates/lt_spec.rb +0 -40
- data/spec/unit/predicates/lteq_spec.rb +0 -40
- data/spec/unit/predicates/max_size_spec.rb +0 -49
- data/spec/unit/predicates/min_size_spec.rb +0 -49
- data/spec/unit/predicates/nil_spec.rb +0 -28
- data/spec/unit/predicates/size_spec.rb +0 -49
- data/spec/unit/predicates/str_spec.rb +0 -32
- data/spec/unit/rule/each_spec.rb +0 -20
- data/spec/unit/rule/key_spec.rb +0 -27
- data/spec/unit/rule/set_spec.rb +0 -32
- data/spec/unit/rule/value_spec.rb +0 -42
- 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)), ¯o.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
|