dry-validation 1.3.1 → 1.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"