dry-validation 1.2.0 → 1.5.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 +4 -4
- data/CHANGELOG.md +277 -153
- 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 +6 -8
- data/lib/dry/validation/config.rb +2 -2
- data/lib/dry/validation/constants.rb +5 -4
- data/lib/dry/validation/contract.rb +19 -13
- data/lib/dry/validation/contract/class_interface.rb +63 -50
- data/lib/dry/validation/evaluator.rb +24 -10
- data/lib/dry/validation/extensions/hints.rb +1 -1
- data/lib/dry/validation/extensions/monads.rb +1 -1
- data/lib/dry/validation/extensions/predicates_as_macros.rb +3 -3
- data/lib/dry/validation/failures.rb +15 -3
- data/lib/dry/validation/function.rb +2 -2
- data/lib/dry/validation/macro.rb +2 -2
- data/lib/dry/validation/macros.rb +2 -2
- data/lib/dry/validation/message.rb +4 -4
- data/lib/dry/validation/message_set.rb +5 -50
- data/lib/dry/validation/messages/resolver.rb +30 -3
- data/lib/dry/validation/result.rb +37 -5
- data/lib/dry/validation/rule.rb +17 -9
- data/lib/dry/validation/schema_ext.rb +6 -21
- data/lib/dry/validation/values.rb +7 -8
- data/lib/dry/validation/version.rb +1 -1
- metadata +27 -28
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "dry/schema/predicate_registry"
|
|
4
|
+
require "dry/validation/contract"
|
|
5
5
|
|
|
6
6
|
module Dry
|
|
7
7
|
module Validation
|
|
@@ -12,7 +12,7 @@ module Dry
|
|
|
12
12
|
#
|
|
13
13
|
# @see Dry::Validation::Contract
|
|
14
14
|
WHITELIST = %i[
|
|
15
|
-
filled? gt? gteq? included_in? includes? inclusion? is? lt?
|
|
15
|
+
filled? format? gt? gteq? included_in? includes? inclusion? is? lt?
|
|
16
16
|
lteq? max_size? min_size? not_eql? odd? respond_to? size? true?
|
|
17
17
|
uuid_v4?
|
|
18
18
|
].freeze
|
|
@@ -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
|
data/lib/dry/validation/macro.rb
CHANGED
|
@@ -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
|
|
|
@@ -92,51 +92,6 @@ module Dry
|
|
|
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,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "dry/validation/message"
|
|
4
4
|
|
|
5
5
|
module Dry
|
|
6
6
|
module Validation
|
|
@@ -22,6 +22,8 @@ module Dry
|
|
|
22
22
|
# Resolve Message object from provided args and path
|
|
23
23
|
#
|
|
24
24
|
# This is used internally by contracts when rules are applied
|
|
25
|
+
# If message argument is a Hash, then it MUST have a :text key,
|
|
26
|
+
# which value will be used as the message value
|
|
25
27
|
#
|
|
26
28
|
# @return [Message, Message::Localized]
|
|
27
29
|
#
|
|
@@ -34,7 +36,12 @@ module Dry
|
|
|
34
36
|
Message[message, path, meta]
|
|
35
37
|
when Hash
|
|
36
38
|
meta = message.dup
|
|
37
|
-
text = meta.delete(:text)
|
|
39
|
+
text = meta.delete(:text) { |key|
|
|
40
|
+
raise ArgumentError, <<~STR
|
|
41
|
+
+message+ Hash must contain :#{key} key (#{message.inspect} given)
|
|
42
|
+
STR
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
call(message: text, tokens: tokens, path: path, meta: meta)
|
|
39
46
|
else
|
|
40
47
|
raise ArgumentError, <<~STR
|
|
@@ -68,11 +75,31 @@ module Dry
|
|
|
68
75
|
STR
|
|
69
76
|
end
|
|
70
77
|
|
|
71
|
-
|
|
78
|
+
parsed_tokens = parse_tokens(tokens)
|
|
79
|
+
text = template.(template.data(parsed_tokens))
|
|
72
80
|
|
|
73
81
|
[full ? "#{messages.rule(keys.last, msg_opts)} #{text}" : text, meta]
|
|
74
82
|
end
|
|
75
83
|
# rubocop:enable Metrics/AbcSize
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def parse_tokens(tokens)
|
|
88
|
+
Hash[
|
|
89
|
+
tokens.map do |key, token|
|
|
90
|
+
[key, parse_token(token)]
|
|
91
|
+
end
|
|
92
|
+
]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def parse_token(token)
|
|
96
|
+
case token
|
|
97
|
+
when Array
|
|
98
|
+
token.join(", ")
|
|
99
|
+
else
|
|
100
|
+
token
|
|
101
|
+
end
|
|
102
|
+
end
|
|
76
103
|
end
|
|
77
104
|
end
|
|
78
105
|
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
|
|
@@ -106,6 +106,22 @@ module Dry
|
|
|
106
106
|
schema_result.error?(key)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# Check if there's any error for the provided key
|
|
110
|
+
#
|
|
111
|
+
# This does not consider errors from the nested values
|
|
112
|
+
#
|
|
113
|
+
# @api private
|
|
114
|
+
def base_error?(key)
|
|
115
|
+
schema_result.errors.any? { |error|
|
|
116
|
+
key_path = Schema::Path[key]
|
|
117
|
+
err_path = Schema::Path[error.path]
|
|
118
|
+
|
|
119
|
+
return false unless key_path.same_root?(err_path)
|
|
120
|
+
|
|
121
|
+
key_path == err_path
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
109
125
|
# Add a new error for the provided key
|
|
110
126
|
#
|
|
111
127
|
# @api private
|
|
@@ -163,6 +179,22 @@ module Dry
|
|
|
163
179
|
super
|
|
164
180
|
end
|
|
165
181
|
|
|
182
|
+
if RUBY_VERSION >= "2.7"
|
|
183
|
+
# Pattern matching
|
|
184
|
+
#
|
|
185
|
+
# @api private
|
|
186
|
+
def deconstruct_keys(keys)
|
|
187
|
+
values.deconstruct_keys(keys)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Pattern matching
|
|
191
|
+
#
|
|
192
|
+
# @api private
|
|
193
|
+
def deconstruct
|
|
194
|
+
[values, context.each.to_h]
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
166
198
|
private
|
|
167
199
|
|
|
168
200
|
# @api private
|
data/lib/dry/validation/rule.rb
CHANGED
|
@@ -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/validation/constants"
|
|
6
|
+
require "dry/validation/function"
|
|
7
7
|
|
|
8
8
|
module Dry
|
|
9
9
|
module Validation
|
|
@@ -82,14 +82,16 @@ module Dry
|
|
|
82
82
|
@keys = []
|
|
83
83
|
|
|
84
84
|
@block = proc do
|
|
85
|
-
(
|
|
86
|
-
|
|
85
|
+
unless result.base_error?(root) || !values.key?(root)
|
|
86
|
+
values[root].each_with_index do |_, idx|
|
|
87
|
+
path = [*Schema::Path[root].to_a, idx]
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
next if result.error?(path)
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
evaluator = with(macros: macros, keys: [path], &block)
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
failures.concat(evaluator.failures)
|
|
94
|
+
end
|
|
93
95
|
end
|
|
94
96
|
end
|
|
95
97
|
|
|
@@ -116,12 +118,18 @@ module Dry
|
|
|
116
118
|
args.each_with_object([]) do |spec, macros|
|
|
117
119
|
case spec
|
|
118
120
|
when Hash
|
|
119
|
-
|
|
121
|
+
add_macro_from_hash(macros, spec)
|
|
120
122
|
else
|
|
121
123
|
macros << Array(spec)
|
|
122
124
|
end
|
|
123
125
|
end
|
|
124
126
|
end
|
|
127
|
+
|
|
128
|
+
def add_macro_from_hash(macros, spec)
|
|
129
|
+
spec.each do |k, v|
|
|
130
|
+
macros << [k, v.is_a?(Array) ? v : [v]]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
125
133
|
end
|
|
126
134
|
end
|
|
127
135
|
end
|
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require 'dry/schema/key_map'
|
|
3
|
+
require "dry/schema/path"
|
|
5
4
|
|
|
6
5
|
module Dry
|
|
7
6
|
module Schema
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
# TODO: this should be moved to dry-schema at some point
|
|
11
|
-
class Key
|
|
7
|
+
class Path
|
|
12
8
|
# @api private
|
|
13
|
-
def
|
|
14
|
-
|
|
9
|
+
def multi_value?
|
|
10
|
+
last.is_a?(Array)
|
|
15
11
|
end
|
|
16
12
|
|
|
17
13
|
# @api private
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def to_dot_notation
|
|
21
|
-
[name].product(members.map(&:to_dot_notation).flatten(1)).map { |e| e.join(DOT) }
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# @api private
|
|
27
|
-
class KeyMap
|
|
28
|
-
# @api private
|
|
29
|
-
def to_dot_notation
|
|
30
|
-
@to_dot_notation ||= map(&:to_dot_notation).flatten
|
|
14
|
+
def expand
|
|
15
|
+
to_a[0..-2].product(last).map { |spec| self.class[spec] }
|
|
31
16
|
end
|
|
32
17
|
end
|
|
33
18
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
3
|
+
require "dry/equalizer"
|
|
4
|
+
require "dry/schema/path"
|
|
5
|
+
require "dry/validation/constants"
|
|
6
6
|
|
|
7
7
|
module Dry
|
|
8
8
|
module Validation
|
|
@@ -45,17 +45,15 @@ module Dry
|
|
|
45
45
|
|
|
46
46
|
case (key = args[0])
|
|
47
47
|
when Symbol, String, Array, Hash
|
|
48
|
-
|
|
49
|
-
keys = path.to_a
|
|
48
|
+
keys = Schema::Path[key].to_a
|
|
50
49
|
|
|
51
50
|
return data.dig(*keys) unless keys.last.is_a?(Array)
|
|
52
51
|
|
|
53
52
|
last = keys.pop
|
|
54
53
|
vals = self.class.new(data.dig(*keys))
|
|
55
|
-
|
|
56
|
-
last.map { |name| vals[name] }
|
|
54
|
+
vals.fetch_values(*last) { nil }
|
|
57
55
|
else
|
|
58
|
-
raise ArgumentError,
|
|
56
|
+
raise ArgumentError, "+key+ must be a valid path specification"
|
|
59
57
|
end
|
|
60
58
|
end
|
|
61
59
|
|
|
@@ -91,6 +89,7 @@ module Dry
|
|
|
91
89
|
super
|
|
92
90
|
end
|
|
93
91
|
end
|
|
92
|
+
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
|
94
93
|
end
|
|
95
94
|
end
|
|
96
95
|
end
|