dry-validation 1.0.0 → 1.5.6
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 +4 -4
- data/CHANGELOG.md +379 -141
- data/LICENSE +1 -1
- data/README.md +7 -8
- data/dry-validation.gemspec +41 -0
- data/lib/dry-validation.rb +1 -1
- data/lib/dry/validation.rb +10 -5
- data/lib/dry/validation/config.rb +2 -2
- data/lib/dry/validation/constants.rb +9 -2
- data/lib/dry/validation/contract.rb +34 -17
- data/lib/dry/validation/contract/class_interface.rb +62 -56
- data/lib/dry/validation/evaluator.rb +50 -13
- data/lib/dry/validation/extensions/hints.rb +1 -3
- data/lib/dry/validation/extensions/monads.rb +1 -1
- data/lib/dry/validation/extensions/predicates_as_macros.rb +75 -0
- data/lib/dry/validation/failures.rb +15 -3
- data/lib/dry/validation/function.rb +13 -10
- data/lib/dry/validation/macro.rb +2 -2
- data/lib/dry/validation/macros.rb +3 -3
- data/lib/dry/validation/message.rb +4 -4
- data/lib/dry/validation/message_set.rb +6 -51
- data/lib/dry/validation/messages/resolver.rb +60 -5
- data/lib/dry/validation/result.rb +47 -8
- data/lib/dry/validation/rule.rb +43 -12
- data/lib/dry/validation/schema_ext.rb +19 -0
- data/lib/dry/validation/values.rb +38 -11
- data/lib/dry/validation/version.rb +1 -1
- metadata +31 -24
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'dry/monads/result'
|
4
|
-
|
5
3
|
module Dry
|
6
4
|
module Validation
|
7
5
|
# Hints extension
|
@@ -46,7 +44,7 @@ module Dry
|
|
46
44
|
#
|
47
45
|
# @api public
|
48
46
|
def messages(new_options = EMPTY_HASH)
|
49
|
-
errors.with(hints.to_a, options.merge(**new_options))
|
47
|
+
errors.with(hints(new_options).to_a, options.merge(**new_options))
|
50
48
|
end
|
51
49
|
|
52
50
|
# Return hint messages
|
@@ -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
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "dry/schema/path"
|
4
|
+
require "dry/validation/constants"
|
5
5
|
|
6
6
|
module Dry
|
7
7
|
module Validation
|
@@ -45,14 +45,26 @@ module Dry
|
|
45
45
|
# @example
|
46
46
|
# failure(:taken)
|
47
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
|
+
#
|
48
55
|
# @see Evaluator#key
|
49
56
|
# @see Evaluator#base
|
50
57
|
#
|
51
58
|
# @api public
|
52
59
|
def failure(message, tokens = EMPTY_HASH)
|
53
|
-
opts << {
|
60
|
+
opts << {message: message, tokens: tokens, path: path}
|
54
61
|
self
|
55
62
|
end
|
63
|
+
|
64
|
+
# @api private
|
65
|
+
def empty?
|
66
|
+
opts.empty?
|
67
|
+
end
|
56
68
|
end
|
57
69
|
end
|
58
70
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "dry/initializer"
|
4
|
+
require "dry/validation/constants"
|
5
5
|
|
6
6
|
module Dry
|
7
7
|
module Validation
|
@@ -19,21 +19,24 @@ module Dry
|
|
19
19
|
# @api private
|
20
20
|
option :block
|
21
21
|
|
22
|
+
# @!attribute [r] block_options
|
23
|
+
# @return [Hash]
|
24
|
+
# @api private
|
25
|
+
option :block_options, default: -> { block ? map_keywords(block) : EMPTY_HASH }
|
26
|
+
|
22
27
|
private
|
23
28
|
|
24
29
|
# Extract options for the block kwargs
|
25
30
|
#
|
26
|
-
# @
|
31
|
+
# @param [Proc] block Callable
|
32
|
+
# @return Hash
|
27
33
|
#
|
28
34
|
# @api private
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
@block_options ||= block
|
35
|
+
def map_keywords(block)
|
36
|
+
block
|
33
37
|
.parameters
|
34
|
-
.select { |arg
|
35
|
-
.map
|
36
|
-
.map { |name| [name, BLOCK_OPTIONS_MAPPINGS[name]] }
|
38
|
+
.select { |arg,| arg.equal?(:keyreq) }
|
39
|
+
.map { |_, name| [name, BLOCK_OPTIONS_MAPPINGS[name]] }
|
37
40
|
.to_h
|
38
41
|
end
|
39
42
|
end
|
data/lib/dry/validation/macro.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "dry/container"
|
4
|
+
require "dry/validation/macro"
|
5
5
|
|
6
6
|
module Dry
|
7
7
|
module Validation
|
@@ -25,7 +25,7 @@ module Dry
|
|
25
25
|
# end
|
26
26
|
#
|
27
27
|
# @param [Symbol] name The name of the macro
|
28
|
-
# @param [Array]
|
28
|
+
# @param [Array] args Optional default positional arguments for the macro
|
29
29
|
#
|
30
30
|
# @return [self]
|
31
31
|
#
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/equalizer"
|
4
4
|
|
5
|
-
require
|
6
|
-
require
|
5
|
+
require "dry/schema/constants"
|
6
|
+
require "dry/schema/message"
|
7
7
|
|
8
8
|
module Dry
|
9
9
|
module Validation
|
@@ -52,7 +52,7 @@ module Dry
|
|
52
52
|
#
|
53
53
|
# @api public
|
54
54
|
def evaluate(**opts)
|
55
|
-
evaluated_text, rest = text.(opts)
|
55
|
+
evaluated_text, rest = text.(**opts)
|
56
56
|
Message.new(evaluated_text, path: path, meta: rest.merge(meta))
|
57
57
|
end
|
58
58
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/schema/message_set"
|
4
4
|
|
5
|
-
require
|
6
|
-
require
|
5
|
+
require "dry/validation/constants"
|
6
|
+
require "dry/validation/message"
|
7
7
|
|
8
8
|
module Dry
|
9
9
|
module Validation
|
@@ -41,7 +41,7 @@ module Dry
|
|
41
41
|
return self if new_options.empty? && other.eql?(messages)
|
42
42
|
|
43
43
|
self.class.new(
|
44
|
-
|
44
|
+
other | select { |err| err.is_a?(Message) },
|
45
45
|
options.merge(source: source_messages, **new_options)
|
46
46
|
).freeze
|
47
47
|
end
|
@@ -54,9 +54,9 @@ module Dry
|
|
54
54
|
#
|
55
55
|
# @api private
|
56
56
|
def add(message)
|
57
|
+
@empty = nil
|
57
58
|
source_messages << message
|
58
59
|
messages << message
|
59
|
-
initialize_placeholders!
|
60
60
|
self
|
61
61
|
end
|
62
62
|
|
@@ -85,58 +85,13 @@ module Dry
|
|
85
85
|
# @api private
|
86
86
|
def freeze
|
87
87
|
source_messages.select { |err| err.respond_to?(:evaluate) }.each do |err|
|
88
|
-
idx = source_messages.index(err)
|
88
|
+
idx = messages.index(err) || source_messages.index(err)
|
89
89
|
msg = err.evaluate(locale: locale, full: options[:full])
|
90
90
|
messages[idx] = msg
|
91
91
|
end
|
92
92
|
to_h
|
93
93
|
self
|
94
94
|
end
|
95
|
-
|
96
|
-
private
|
97
|
-
|
98
|
-
# @api private
|
99
|
-
def unique_paths
|
100
|
-
source_messages.uniq(&:path).map(&:path)
|
101
|
-
end
|
102
|
-
|
103
|
-
# @api private
|
104
|
-
def messages_map
|
105
|
-
@messages_map ||= reduce(placeholders) { |hash, msg|
|
106
|
-
node = msg.path.reduce(hash) { |a, e| a.is_a?(Hash) ? a[e] : a.last[e] }
|
107
|
-
(node[0].is_a?(::Array) ? node[0] : node) << msg.dump
|
108
|
-
hash
|
109
|
-
}
|
110
|
-
end
|
111
|
-
|
112
|
-
# @api private
|
113
|
-
#
|
114
|
-
# rubocop:disable Metrics/AbcSize
|
115
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
116
|
-
def initialize_placeholders!
|
117
|
-
@placeholders = unique_paths.each_with_object(EMPTY_HASH.dup) { |path, hash|
|
118
|
-
curr_idx = 0
|
119
|
-
last_idx = path.size - 1
|
120
|
-
node = hash
|
121
|
-
|
122
|
-
while curr_idx <= last_idx
|
123
|
-
key = path[curr_idx]
|
124
|
-
|
125
|
-
next_node =
|
126
|
-
if node.is_a?(Array) && key.is_a?(Symbol)
|
127
|
-
node_hash = (node << [] << {}).last
|
128
|
-
node_hash[key] || (node_hash[key] = curr_idx < last_idx ? {} : [])
|
129
|
-
else
|
130
|
-
node[key] || (node[key] = curr_idx < last_idx ? {} : [])
|
131
|
-
end
|
132
|
-
|
133
|
-
node = next_node
|
134
|
-
curr_idx += 1
|
135
|
-
end
|
136
|
-
}
|
137
|
-
end
|
138
|
-
# rubocop:enable Metrics/AbcSize
|
139
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
140
95
|
end
|
141
96
|
end
|
142
97
|
end
|
@@ -1,10 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/validation/message"
|
4
|
+
require "dry/schema/message_compiler"
|
4
5
|
|
5
6
|
module Dry
|
6
7
|
module Validation
|
7
8
|
module Messages
|
9
|
+
FULL_MESSAGE_WHITESPACE = Dry::Schema::MessageCompiler::FULL_MESSAGE_WHITESPACE
|
10
|
+
|
8
11
|
# Resolve translated messages from failure arguments
|
9
12
|
#
|
10
13
|
# @api public
|
@@ -22,6 +25,8 @@ module Dry
|
|
22
25
|
# Resolve Message object from provided args and path
|
23
26
|
#
|
24
27
|
# This is used internally by contracts when rules are applied
|
28
|
+
# If message argument is a Hash, then it MUST have a :text key,
|
29
|
+
# which value will be used as the message value
|
25
30
|
#
|
26
31
|
# @return [Message, Message::Localized]
|
27
32
|
#
|
@@ -31,10 +36,15 @@ module Dry
|
|
31
36
|
when Symbol
|
32
37
|
Message[->(**opts) { message(message, path: path, tokens: tokens, **opts) }, path, meta]
|
33
38
|
when String
|
34
|
-
Message[message, path, meta]
|
39
|
+
Message[->(**opts) { [message_text(message, path: path, **opts), meta] }, path, meta]
|
35
40
|
when Hash
|
36
41
|
meta = message.dup
|
37
|
-
text = meta.delete(:text)
|
42
|
+
text = meta.delete(:text) { |key|
|
43
|
+
raise ArgumentError, <<~STR
|
44
|
+
+message+ Hash must contain :#{key} key (#{message.inspect} given)
|
45
|
+
STR
|
46
|
+
}
|
47
|
+
|
38
48
|
call(message: text, tokens: tokens, path: path, meta: meta)
|
39
49
|
else
|
40
50
|
raise ArgumentError, <<~STR
|
@@ -49,6 +59,8 @@ module Dry
|
|
49
59
|
# @return [String]
|
50
60
|
#
|
51
61
|
# @api public
|
62
|
+
#
|
63
|
+
# rubocop:disable Metrics/AbcSize
|
52
64
|
def message(rule, tokens: EMPTY_HASH, locale: nil, full: false, path:)
|
53
65
|
keys = path.to_a.compact
|
54
66
|
msg_opts = tokens.merge(path: keys, locale: locale || messages.default_locale)
|
@@ -60,15 +72,58 @@ module Dry
|
|
60
72
|
template, meta = messages[rule, msg_opts.merge(path: keys.last)] unless template
|
61
73
|
end
|
62
74
|
|
75
|
+
if !template && keys.size > 1
|
76
|
+
non_index_keys = keys.reject { |k| k.is_a?(Integer) }
|
77
|
+
template, meta = messages[rule, msg_opts.merge(path: non_index_keys.join(DOT))]
|
78
|
+
end
|
79
|
+
|
63
80
|
unless template
|
64
81
|
raise MissingMessageError, <<~STR
|
65
82
|
Message template for #{rule.inspect} under #{keys.join(DOT).inspect} was not found
|
66
83
|
STR
|
67
84
|
end
|
68
85
|
|
69
|
-
|
86
|
+
parsed_tokens = parse_tokens(tokens)
|
87
|
+
text = template.(template.data(parsed_tokens))
|
70
88
|
|
71
|
-
[
|
89
|
+
[message_text(text, path: path, locale: locale, full: full), meta]
|
90
|
+
end
|
91
|
+
# rubocop:enable Metrics/AbcSize
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def message_text(text, path:, locale: nil, full: false)
|
96
|
+
return text unless full
|
97
|
+
|
98
|
+
key = key_text(path: path, locale: locale)
|
99
|
+
|
100
|
+
[key, text].compact.join(FULL_MESSAGE_WHITESPACE[locale])
|
101
|
+
end
|
102
|
+
|
103
|
+
def key_text(path:, locale: nil)
|
104
|
+
locale ||= messages.default_locale
|
105
|
+
|
106
|
+
keys = path.to_a.compact
|
107
|
+
msg_opts = {path: keys, locale: locale}
|
108
|
+
|
109
|
+
messages.rule(keys.last, msg_opts) || keys.last
|
110
|
+
end
|
111
|
+
|
112
|
+
def parse_tokens(tokens)
|
113
|
+
Hash[
|
114
|
+
tokens.map do |key, token|
|
115
|
+
[key, parse_token(token)]
|
116
|
+
end
|
117
|
+
]
|
118
|
+
end
|
119
|
+
|
120
|
+
def parse_token(token)
|
121
|
+
case token
|
122
|
+
when Array
|
123
|
+
token.join(", ")
|
124
|
+
else
|
125
|
+
token
|
126
|
+
end
|
72
127
|
end
|
73
128
|
end
|
74
129
|
end
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "concurrent/map"
|
4
|
+
require "dry/equalizer"
|
5
5
|
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
6
|
+
require "dry/validation/constants"
|
7
|
+
require "dry/validation/message_set"
|
8
|
+
require "dry/validation/values"
|
9
9
|
|
10
10
|
module Dry
|
11
11
|
module Validation
|
@@ -101,11 +101,34 @@ module Dry
|
|
101
101
|
|
102
102
|
# Check if values include an error for the provided key
|
103
103
|
#
|
104
|
-
# @api
|
104
|
+
# @api public
|
105
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)
|
106
113
|
schema_result.error?(key)
|
107
114
|
end
|
108
115
|
|
116
|
+
# Check if there's any error for the provided key
|
117
|
+
#
|
118
|
+
# This does not consider errors from the nested values
|
119
|
+
#
|
120
|
+
# @api private
|
121
|
+
def base_error?(key)
|
122
|
+
schema_result.errors.any? { |error|
|
123
|
+
key_path = Schema::Path[key]
|
124
|
+
err_path = Schema::Path[error.path]
|
125
|
+
|
126
|
+
next unless key_path.same_root?(err_path)
|
127
|
+
|
128
|
+
key_path == err_path
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
109
132
|
# Add a new error for the provided key
|
110
133
|
#
|
111
134
|
# @api private
|
@@ -148,9 +171,9 @@ module Dry
|
|
148
171
|
# @api public
|
149
172
|
def inspect
|
150
173
|
if context.empty?
|
151
|
-
"#<#{self.class}#{to_h
|
174
|
+
"#<#{self.class}#{to_h} errors=#{errors.to_h}>"
|
152
175
|
else
|
153
|
-
"#<#{self.class}#{to_h
|
176
|
+
"#<#{self.class}#{to_h} errors=#{errors.to_h} context=#{context.each.to_h}>"
|
154
177
|
end
|
155
178
|
end
|
156
179
|
|
@@ -163,6 +186,22 @@ module Dry
|
|
163
186
|
super
|
164
187
|
end
|
165
188
|
|
189
|
+
if RUBY_VERSION >= "2.7"
|
190
|
+
# Pattern matching
|
191
|
+
#
|
192
|
+
# @api private
|
193
|
+
def deconstruct_keys(keys)
|
194
|
+
values.deconstruct_keys(keys)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Pattern matching
|
198
|
+
#
|
199
|
+
# @api private
|
200
|
+
def deconstruct
|
201
|
+
[values, context.each.to_h]
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
166
205
|
private
|
167
206
|
|
168
207
|
# @api private
|