dry-validation 1.5.3

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