dry-validation 1.3.1 → 1.8.1

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.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/container'
4
- require 'dry/validation/macro'
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] *args Optional default arguments for the macro
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 'dry/equalizer'
3
+ require "dry/core/equalizer"
4
4
 
5
- require 'dry/schema/constants'
6
- require 'dry/schema/message'
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
@@ -70,11 +70,13 @@ module Dry
70
70
  # Initialize a new error object
71
71
  #
72
72
  # @api private
73
+ # rubocop: disable Lint/MissingSuper
73
74
  def initialize(text, path:, meta: EMPTY_HASH)
74
75
  @text = text
75
76
  @path = Array(path)
76
77
  @meta = meta
77
78
  end
79
+ # rubocop: enable Lint/MissingSuper
78
80
 
79
81
  # Check if this is a base error not associated with any key
80
82
  #
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/message_set'
3
+ require "dry/schema/message_set"
4
4
 
5
- require 'dry/validation/constants'
6
- require 'dry/validation/message'
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
- (other + select { |err| err.is_a?(Message) }).uniq,
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 'dry/validation/message'
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
@@ -51,7 +61,8 @@ module Dry
51
61
  # @api public
52
62
  #
53
63
  # rubocop:disable Metrics/AbcSize
54
- def message(rule, tokens: EMPTY_HASH, locale: nil, full: false, path:)
64
+ # rubocop:disable Metrics/PerceivedComplexity
65
+ def message(rule, path:, tokens: EMPTY_HASH, locale: nil, full: false)
55
66
  keys = path.to_a.compact
56
67
  msg_opts = tokens.merge(path: keys, locale: locale || messages.default_locale)
57
68
 
@@ -62,17 +73,56 @@ module Dry
62
73
  template, meta = messages[rule, msg_opts.merge(path: keys.last)] unless template
63
74
  end
64
75
 
76
+ if !template && keys.size > 1
77
+ non_index_keys = keys.reject { |k| k.is_a?(Integer) }
78
+ template, meta = messages[rule, msg_opts.merge(path: non_index_keys.join(DOT))]
79
+ end
80
+
65
81
  unless template
66
82
  raise MissingMessageError, <<~STR
67
83
  Message template for #{rule.inspect} under #{keys.join(DOT).inspect} was not found
68
84
  STR
69
85
  end
70
86
 
71
- text = template.(template.data(tokens))
87
+ parsed_tokens = parse_tokens(tokens)
88
+ text = template.(template.data(parsed_tokens))
72
89
 
73
- [full ? "#{messages.rule(keys.last, msg_opts)} #{text}" : text, meta]
90
+ [message_text(text, path: path, locale: locale, full: full), meta]
74
91
  end
92
+ # rubocop:enable Metrics/PerceivedComplexity
75
93
  # rubocop:enable Metrics/AbcSize
94
+
95
+ private
96
+
97
+ def message_text(text, path:, locale: nil, full: false)
98
+ return text unless full
99
+
100
+ key = key_text(path: path, locale: locale)
101
+
102
+ [key, text].compact.join(FULL_MESSAGE_WHITESPACE[locale])
103
+ end
104
+
105
+ def key_text(path:, locale: nil)
106
+ locale ||= messages.default_locale
107
+
108
+ keys = path.to_a.compact
109
+ msg_opts = {path: keys, locale: locale}
110
+
111
+ messages.rule(keys.last, msg_opts) || keys.last
112
+ end
113
+
114
+ def parse_tokens(tokens)
115
+ tokens.transform_values { parse_token(_1) }
116
+ end
117
+
118
+ def parse_token(token)
119
+ case token
120
+ when Array
121
+ token.join(", ")
122
+ else
123
+ token
124
+ end
125
+ end
76
126
  end
77
127
  end
78
128
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/map'
4
- require 'dry/equalizer'
3
+ require "concurrent/map"
4
+ require "dry/core/equalizer"
5
5
 
6
- require 'dry/validation/constants'
7
- require 'dry/validation/message_set'
8
- require 'dry/validation/values'
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,32 @@ module Dry
101
101
 
102
102
  # Check if values include an error for the provided key
103
103
  #
104
- # @api private
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 the rules includes an error for the provided key
117
+ #
118
+ # @api private
119
+ def rule_error?(key)
120
+ !schema_error?(key) && error?(key)
121
+ end
122
+
123
+ # Check if the result contains any base rule errors
124
+ #
125
+ # @api private
126
+ def base_rule_error?
127
+ !errors.filter(:base?).empty?
128
+ end
129
+
109
130
  # Check if there's any error for the provided key
110
131
  #
111
132
  # This does not consider errors from the nested values
@@ -116,7 +137,7 @@ module Dry
116
137
  key_path = Schema::Path[key]
117
138
  err_path = Schema::Path[error.path]
118
139
 
119
- return false unless key_path.same_root?(err_path)
140
+ next unless key_path.same_root?(err_path)
120
141
 
121
142
  key_path == err_path
122
143
  }
@@ -179,6 +200,22 @@ module Dry
179
200
  super
180
201
  end
181
202
 
203
+ if RUBY_VERSION >= "2.7"
204
+ # Pattern matching
205
+ #
206
+ # @api private
207
+ def deconstruct_keys(keys)
208
+ values.deconstruct_keys(keys)
209
+ end
210
+
211
+ # Pattern matching
212
+ #
213
+ # @api private
214
+ def deconstruct
215
+ [values, context.each.to_h]
216
+ end
217
+ end
218
+
182
219
  private
183
220
 
184
221
  # @api private
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
3
+ require "dry/core/equalizer"
4
4
 
5
- require 'dry/validation/constants'
6
- require 'dry/validation/function'
5
+ require "dry/validation/constants"
6
+ require "dry/validation/function"
7
7
 
8
8
  module Dry
9
9
  module Validation
@@ -65,8 +65,8 @@ module Dry
65
65
  # for a given array item.
66
66
  #
67
67
  # @example
68
- # rule(:nums).each do
69
- # key.failure("must be greater than 0") if value < 0
68
+ # rule(:nums).each do |index:|
69
+ # key([:number, index]).failure("must be greater than 0") if value < 0
70
70
  # end
71
71
  # rule(:nums).each(min: 3)
72
72
  # rule(address: :city) do
@@ -76,19 +76,21 @@ module Dry
76
76
  # @return [Rule]
77
77
  #
78
78
  # @api public
79
+ #
80
+ # rubocop:disable Metrics/AbcSize
79
81
  def each(*macros, &block)
80
82
  root = keys[0]
81
83
  macros = parse_macros(*macros)
82
84
  @keys = []
83
85
 
84
86
  @block = proc do
85
- unless result.base_error?(root) || !values.key?(root)
87
+ unless result.base_error?(root) || !values.key?(root) || values[root].nil?
86
88
  values[root].each_with_index do |_, idx|
87
89
  path = [*Schema::Path[root].to_a, idx]
88
90
 
89
- next if result.error?(path)
91
+ next if result.schema_error?(path)
90
92
 
91
- evaluator = with(macros: macros, keys: [path], &block)
93
+ evaluator = with(macros: macros, keys: [path], index: idx, &block)
92
94
 
93
95
  failures.concat(evaluator.failures)
94
96
  end
@@ -99,6 +101,7 @@ module Dry
99
101
 
100
102
  self
101
103
  end
104
+ # rubocop:enable Metrics/AbcSize
102
105
 
103
106
  # Return a nice string representation
104
107
  #
@@ -118,12 +121,18 @@ module Dry
118
121
  args.each_with_object([]) do |spec, macros|
119
122
  case spec
120
123
  when Hash
121
- spec.each { |k, v| macros << [k, Array(v)] }
124
+ add_macro_from_hash(macros, spec)
122
125
  else
123
126
  macros << Array(spec)
124
127
  end
125
128
  end
126
129
  end
130
+
131
+ def add_macro_from_hash(macros, spec)
132
+ spec.each do |k, v|
133
+ macros << [k, v.is_a?(Array) ? v : [v]]
134
+ end
135
+ end
127
136
  end
128
137
  end
129
138
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/key'
4
- require 'dry/schema/key_map'
3
+ require "dry/schema/path"
5
4
 
6
5
  module Dry
7
6
  module Schema
@@ -16,31 +15,5 @@ module Dry
16
15
  to_a[0..-2].product(last).map { |spec| self.class[spec] }
17
16
  end
18
17
  end
19
-
20
- # @api private
21
- #
22
- # TODO: this should be moved to dry-schema at some point
23
- class Key
24
- # @api private
25
- def to_dot_notation
26
- [name.to_s]
27
- end
28
-
29
- # @api private
30
- class Hash < Key
31
- # @api private
32
- def to_dot_notation
33
- [name].product(members.map(&:to_dot_notation).flatten(1)).map { |e| e.join(DOT) }
34
- end
35
- end
36
- end
37
-
38
- # @api private
39
- class KeyMap
40
- # @api private
41
- def to_dot_notation
42
- @to_dot_notation ||= map(&:to_dot_notation).flatten
43
- end
44
- end
45
18
  end
46
19
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
4
- require 'dry/schema/path'
5
- require 'dry/validation/constants'
3
+ require "dry/core/equalizer"
4
+ require "dry/schema/path"
5
+ require "dry/validation/constants"
6
6
 
7
7
  module Dry
8
8
  module Validation
@@ -35,7 +35,10 @@ module Dry
35
35
  # key.failure('must be > 18') if values[:age] <= 18
36
36
  # end
37
37
  #
38
- # @param [Symbol] key
38
+ # @param args [Symbol, String, Hash, Array<Symbol>] If given as a single
39
+ # Symbol, String, Array or Hash, build a key array using
40
+ # {Dry::Schema::Path} digging for data. If given as positional
41
+ # arguments, use these with Hash#dig on the data directly.
39
42
  #
40
43
  # @return [Object]
41
44
  #
@@ -53,11 +56,12 @@ module Dry
53
56
  vals = self.class.new(data.dig(*keys))
54
57
  vals.fetch_values(*last) { nil }
55
58
  else
56
- raise ArgumentError, '+key+ must be a valid path specification'
59
+ raise ArgumentError, "+key+ must be a valid path specification"
57
60
  end
58
61
  end
59
62
 
60
63
  # @api public
64
+ # rubocop: disable Metrics/PerceivedComplexity
61
65
  def key?(key, hash = data)
62
66
  return hash.key?(key) if key.is_a?(Symbol)
63
67
 
@@ -65,14 +69,20 @@ module Dry
65
69
  if e.is_a?(Array)
66
70
  result = e.all? { |k| key?(k, a) }
67
71
  return result
72
+ elsif e.is_a?(Symbol) && a.is_a?(Array)
73
+ return false
74
+ elsif a.nil?
75
+ return false
76
+ elsif a.is_a?(String)
77
+ return false
68
78
  else
69
79
  return false unless a.is_a?(Array) ? (e >= 0 && e < a.size) : a.key?(e)
70
80
  end
71
81
  a[e]
72
82
  end
73
-
74
83
  true
75
84
  end
85
+ # rubocop: enable Metrics/PerceivedComplexity
76
86
 
77
87
  # @api private
78
88
  def respond_to_missing?(meth, include_private = false)
@@ -89,6 +99,7 @@ module Dry
89
99
  super
90
100
  end
91
101
  end
102
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
92
103
  end
93
104
  end
94
105
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module Validation
5
- VERSION = '1.3.1'
5
+ VERSION = "1.8.1"
6
6
  end
7
7
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/validation/constants'
4
- require 'dry/validation/contract'
5
- require 'dry/validation/macros'
3
+ require "dry/validation/constants"
4
+ require "dry/validation/contract"
5
+ require "dry/validation/macros"
6
6
 
7
7
  # Main namespace
8
8
  #
@@ -16,15 +16,15 @@ module Dry
16
16
  extend Macros::Registrar
17
17
 
18
18
  register_extension(:monads) do
19
- require 'dry/validation/extensions/monads'
19
+ require "dry/validation/extensions/monads"
20
20
  end
21
21
 
22
22
  register_extension(:hints) do
23
- require 'dry/validation/extensions/hints'
23
+ require "dry/validation/extensions/hints"
24
24
  end
25
25
 
26
26
  register_extension(:predicates_as_macros) do
27
- require 'dry/validation/extensions/predicates_as_macros'
27
+ require "dry/validation/extensions/predicates_as_macros"
28
28
  end
29
29
 
30
30
  # Define a contract and build its instance
@@ -46,11 +46,9 @@ module Dry
46
46
  #
47
47
  # @api public
48
48
  #
49
- # rubocop:disable Naming/MethodName
50
49
  def self.Contract(options = EMPTY_HASH, &block)
51
50
  Contract.build(options, &block)
52
51
  end
53
- # rubocop:enable Naming/MethodName
54
52
 
55
53
  # This is needed by Macros::Registrar
56
54
  #
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/validation'
3
+ require "dry/validation"