dry-validation 1.5.3

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.
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+ require "dry/equalizer"
5
+
6
+ require "dry/validation/constants"
7
+ require "dry/validation/message_set"
8
+ require "dry/validation/values"
9
+
10
+ module Dry
11
+ module Validation
12
+ # Result objects are returned by contracts
13
+ #
14
+ # @api public
15
+ class Result
16
+ include Dry::Equalizer(:schema_result, :context, :errors, inspect: false)
17
+
18
+ # Build a new result
19
+ #
20
+ # @param [Dry::Schema::Result] schema_result
21
+ #
22
+ # @api private
23
+ def self.new(schema_result, context = ::Concurrent::Map.new, options = EMPTY_HASH)
24
+ result = super
25
+ yield(result) if block_given?
26
+ result.freeze
27
+ end
28
+
29
+ # Context that's shared between rules
30
+ #
31
+ # @return [Concurrent::Map]
32
+ #
33
+ # @api public
34
+ attr_reader :context
35
+
36
+ # Result from contract's schema
37
+ #
38
+ # @return [Dry::Schema::Result]
39
+ #
40
+ # @api private
41
+ attr_reader :schema_result
42
+
43
+ # Result options
44
+ #
45
+ # @return [Hash]
46
+ #
47
+ # @api private
48
+ attr_reader :options
49
+
50
+ # Initialize a new result
51
+ #
52
+ # @api private
53
+ def initialize(schema_result, context, options)
54
+ @schema_result = schema_result
55
+ @context = context
56
+ @options = options
57
+ @errors = initialize_errors
58
+ end
59
+
60
+ # Return values wrapper with the input processed by schema
61
+ #
62
+ # @return [Values]
63
+ #
64
+ # @api public
65
+ def values
66
+ @values ||= Values.new(schema_result.to_h)
67
+ end
68
+
69
+ # Get error set
70
+ #
71
+ # @!macro errors-options
72
+ # @param [Hash] new_options
73
+ # @option new_options [Symbol] :locale Set locale for messages
74
+ # @option new_options [Boolean] :hints Enable/disable hints
75
+ # @option new_options [Boolean] :full Get messages that include key names
76
+ #
77
+ # @return [MessageSet]
78
+ #
79
+ # @api public
80
+ def errors(new_options = EMPTY_HASH)
81
+ new_options.empty? ? @errors : @errors.with(schema_errors(new_options), new_options)
82
+ end
83
+
84
+ # Check if result is successful
85
+ #
86
+ # @return [Bool]
87
+ #
88
+ # @api public
89
+ def success?
90
+ @errors.empty?
91
+ end
92
+
93
+ # Check if result is not successful
94
+ #
95
+ # @return [Bool]
96
+ #
97
+ # @api public
98
+ def failure?
99
+ !success?
100
+ end
101
+
102
+ # Check if values include an error for the provided key
103
+ #
104
+ # @api public
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)
113
+ schema_result.error?(key)
114
+ end
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
+
132
+ # Add a new error for the provided key
133
+ #
134
+ # @api private
135
+ def add_error(error)
136
+ @errors.add(error)
137
+ self
138
+ end
139
+
140
+ # Read a value under provided key
141
+ #
142
+ # @param [Symbol] key
143
+ #
144
+ # @return [Object]
145
+ #
146
+ # @api public
147
+ def [](key)
148
+ values[key]
149
+ end
150
+
151
+ # Check if a key was set
152
+ #
153
+ # @param [Symbol] key
154
+ #
155
+ # @return [Bool]
156
+ #
157
+ # @api public
158
+ def key?(key)
159
+ values.key?(key)
160
+ end
161
+
162
+ # Coerce to a hash
163
+ #
164
+ # @api public
165
+ def to_h
166
+ values.to_h
167
+ end
168
+
169
+ # Return a string representation
170
+ #
171
+ # @api public
172
+ def inspect
173
+ if context.empty?
174
+ "#<#{self.class}#{to_h} errors=#{errors.to_h}>"
175
+ else
176
+ "#<#{self.class}#{to_h} errors=#{errors.to_h} context=#{context.each.to_h}>"
177
+ end
178
+ end
179
+
180
+ # Freeze result and its error set
181
+ #
182
+ # @api private
183
+ def freeze
184
+ values.freeze
185
+ errors.freeze
186
+ super
187
+ end
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
+
205
+ private
206
+
207
+ # @api private
208
+ def initialize_errors(options = self.options)
209
+ MessageSet.new(schema_errors(options), options)
210
+ end
211
+
212
+ # @api private
213
+ def schema_errors(options)
214
+ schema_result.message_set(options).to_a
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/equalizer"
4
+
5
+ require "dry/validation/constants"
6
+ require "dry/validation/function"
7
+
8
+ module Dry
9
+ module Validation
10
+ # Rules capture configuration and evaluator blocks
11
+ #
12
+ # When a rule is applied, it creates an `Evaluator` using schema result and its
13
+ # block will be evaluated in the context of the evaluator.
14
+ #
15
+ # @see Contract#rule
16
+ #
17
+ # @api public
18
+ class Rule < Function
19
+ include Dry::Equalizer(:keys, :block, inspect: false)
20
+
21
+ # @!attribute [r] keys
22
+ # @return [Array<Symbol, String, Hash>]
23
+ # @api private
24
+ option :keys
25
+
26
+ # @!attribute [r] macros
27
+ # @return [Array<Symbol>]
28
+ # @api private
29
+ option :macros, default: proc { EMPTY_ARRAY.dup }
30
+
31
+ # Evaluate the rule within the provided context
32
+ #
33
+ # @param [Contract] contract
34
+ # @param [Result] result
35
+ #
36
+ # @api private
37
+ def call(contract, result)
38
+ Evaluator.new(
39
+ contract,
40
+ keys: keys,
41
+ macros: macros,
42
+ block_options: block_options,
43
+ result: result,
44
+ values: result.values,
45
+ _context: result.context,
46
+ &block
47
+ )
48
+ end
49
+
50
+ # Define which macros should be executed
51
+ #
52
+ # @see Contract#rule
53
+ # @return [Rule]
54
+ #
55
+ # @api public
56
+ def validate(*macros, &block)
57
+ @macros = parse_macros(*macros)
58
+ @block = block if block
59
+ self
60
+ end
61
+
62
+ # Define a validation function for each element of an array
63
+ #
64
+ # The function will be applied only if schema checks passed
65
+ # for a given array item.
66
+ #
67
+ # @example
68
+ # rule(:nums).each do |index:|
69
+ # key([:number, index]).failure("must be greater than 0") if value < 0
70
+ # end
71
+ # rule(:nums).each(min: 3)
72
+ # rule(address: :city) do
73
+ # key.failure("oops") if value != 'Munich'
74
+ # end
75
+ #
76
+ # @return [Rule]
77
+ #
78
+ # @api public
79
+ def each(*macros, &block)
80
+ root = keys[0]
81
+ macros = parse_macros(*macros)
82
+ @keys = []
83
+
84
+ @block = proc do
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]
88
+
89
+ next if result.schema_error?(path)
90
+
91
+ evaluator = with(macros: macros, keys: [path], index: idx, &block)
92
+
93
+ failures.concat(evaluator.failures)
94
+ end
95
+ end
96
+ end
97
+
98
+ @block_options = map_keywords(block) if block
99
+
100
+ self
101
+ end
102
+
103
+ # Return a nice string representation
104
+ #
105
+ # @return [String]
106
+ #
107
+ # @api public
108
+ def inspect
109
+ %(#<#{self.class} keys=#{keys.inspect}>)
110
+ end
111
+
112
+ # Parse function arguments into macros structure
113
+ #
114
+ # @return [Array]
115
+ #
116
+ # @api private
117
+ def parse_macros(*args)
118
+ args.each_with_object([]) do |spec, macros|
119
+ case spec
120
+ when Hash
121
+ add_macro_from_hash(macros, spec)
122
+ else
123
+ macros << Array(spec)
124
+ end
125
+ end
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
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/path"
4
+
5
+ module Dry
6
+ module Schema
7
+ class Path
8
+ # @api private
9
+ def multi_value?
10
+ last.is_a?(Array)
11
+ end
12
+
13
+ # @api private
14
+ def expand
15
+ to_a[0..-2].product(last).map { |spec| self.class[spec] }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/equalizer"
4
+ require "dry/schema/path"
5
+ require "dry/validation/constants"
6
+
7
+ module Dry
8
+ module Validation
9
+ # A convenient wrapper for data processed by schemas
10
+ #
11
+ # Values are available within the rule blocks. They act as hash-like
12
+ # objects and expose a convenient API for accessing data.
13
+ #
14
+ # @api public
15
+ class Values
16
+ include Enumerable
17
+ include Dry::Equalizer(:data)
18
+
19
+ # Schema's result output
20
+ #
21
+ # @return [Hash]
22
+ #
23
+ # @api private
24
+ attr_reader :data
25
+
26
+ # @api private
27
+ def initialize(data)
28
+ @data = data
29
+ end
30
+
31
+ # Read from the provided key
32
+ #
33
+ # @example
34
+ # rule(:age) do
35
+ # key.failure('must be > 18') if values[:age] <= 18
36
+ # end
37
+ #
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.
42
+ #
43
+ # @return [Object]
44
+ #
45
+ # @api public
46
+ def [](*args)
47
+ return data.dig(*args) if args.size > 1
48
+
49
+ case (key = args[0])
50
+ when Symbol, String, Array, Hash
51
+ keys = Schema::Path[key].to_a
52
+
53
+ return data.dig(*keys) unless keys.last.is_a?(Array)
54
+
55
+ last = keys.pop
56
+ vals = self.class.new(data.dig(*keys))
57
+ vals.fetch_values(*last) { nil }
58
+ else
59
+ raise ArgumentError, "+key+ must be a valid path specification"
60
+ end
61
+ end
62
+
63
+ # @api public
64
+ def key?(key, hash = data)
65
+ return hash.key?(key) if key.is_a?(Symbol)
66
+
67
+ Schema::Path[key].reduce(hash) do |a, e|
68
+ if e.is_a?(Array)
69
+ result = e.all? { |k| key?(k, a) }
70
+ return result
71
+ elsif e.is_a?(Symbol) && a.is_a?(Array)
72
+ return false
73
+ else
74
+ return false unless a.is_a?(Array) ? (e >= 0 && e < a.size) : a.key?(e)
75
+ end
76
+ a[e]
77
+ end
78
+
79
+ true
80
+ end
81
+
82
+ # @api private
83
+ def respond_to_missing?(meth, include_private = false)
84
+ super || data.respond_to?(meth, include_private)
85
+ end
86
+
87
+ private
88
+
89
+ # @api private
90
+ def method_missing(meth, *args, &block)
91
+ if data.respond_to?(meth)
92
+ data.public_send(meth, *args, &block)
93
+ else
94
+ super
95
+ end
96
+ end
97
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
98
+ end
99
+ end
100
+ end