dry-validation 1.3.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.
@@ -0,0 +1,195 @@
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 private
105
+ def error?(key)
106
+ schema_result.error?(key)
107
+ end
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
+
125
+ # Add a new error for the provided key
126
+ #
127
+ # @api private
128
+ def add_error(error)
129
+ @errors.add(error)
130
+ self
131
+ end
132
+
133
+ # Read a value under provided key
134
+ #
135
+ # @param [Symbol] key
136
+ #
137
+ # @return [Object]
138
+ #
139
+ # @api public
140
+ def [](key)
141
+ values[key]
142
+ end
143
+
144
+ # Check if a key was set
145
+ #
146
+ # @param [Symbol] key
147
+ #
148
+ # @return [Bool]
149
+ #
150
+ # @api public
151
+ def key?(key)
152
+ values.key?(key)
153
+ end
154
+
155
+ # Coerce to a hash
156
+ #
157
+ # @api public
158
+ def to_h
159
+ values.to_h
160
+ end
161
+
162
+ # Return a string representation
163
+ #
164
+ # @api public
165
+ def inspect
166
+ if context.empty?
167
+ "#<#{self.class}#{to_h} errors=#{errors.to_h}>"
168
+ else
169
+ "#<#{self.class}#{to_h} errors=#{errors.to_h} context=#{context.each.to_h}>"
170
+ end
171
+ end
172
+
173
+ # Freeze result and its error set
174
+ #
175
+ # @api private
176
+ def freeze
177
+ values.freeze
178
+ errors.freeze
179
+ super
180
+ end
181
+
182
+ private
183
+
184
+ # @api private
185
+ def initialize_errors(options = self.options)
186
+ MessageSet.new(schema_errors(options), options)
187
+ end
188
+
189
+ # @api private
190
+ def schema_errors(options)
191
+ schema_result.message_set(options).to_a
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,129 @@
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
69
+ # key.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.error?(path)
90
+
91
+ evaluator = with(macros: macros, keys: [path], &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
+ spec.each { |k, v| macros << [k, Array(v)] }
122
+ else
123
+ macros << Array(spec)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/schema/key'
4
+ require 'dry/schema/key_map'
5
+
6
+ module Dry
7
+ module Schema
8
+ class Path
9
+ # @api private
10
+ def multi_value?
11
+ last.is_a?(Array)
12
+ end
13
+
14
+ # @api private
15
+ def expand
16
+ to_a[0..-2].product(last).map { |spec| self.class[spec] }
17
+ end
18
+ 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
+ end
46
+ end
@@ -0,0 +1,94 @@
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 [Symbol] key
39
+ #
40
+ # @return [Object]
41
+ #
42
+ # @api public
43
+ def [](*args)
44
+ return data.dig(*args) if args.size > 1
45
+
46
+ case (key = args[0])
47
+ when Symbol, String, Array, Hash
48
+ keys = Schema::Path[key].to_a
49
+
50
+ return data.dig(*keys) unless keys.last.is_a?(Array)
51
+
52
+ last = keys.pop
53
+ vals = self.class.new(data.dig(*keys))
54
+ vals.fetch_values(*last) { nil }
55
+ else
56
+ raise ArgumentError, '+key+ must be a valid path specification'
57
+ end
58
+ end
59
+
60
+ # @api public
61
+ def key?(key, hash = data)
62
+ return hash.key?(key) if key.is_a?(Symbol)
63
+
64
+ Schema::Path[key].reduce(hash) do |a, e|
65
+ if e.is_a?(Array)
66
+ result = e.all? { |k| key?(k, a) }
67
+ return result
68
+ else
69
+ return false unless a.is_a?(Array) ? (e >= 0 && e < a.size) : a.key?(e)
70
+ end
71
+ a[e]
72
+ end
73
+
74
+ true
75
+ end
76
+
77
+ # @api private
78
+ def respond_to_missing?(meth, include_private = false)
79
+ super || data.respond_to?(meth, include_private)
80
+ end
81
+
82
+ private
83
+
84
+ # @api private
85
+ def method_missing(meth, *args, &block)
86
+ if data.respond_to?(meth)
87
+ data.public_send(meth, *args, &block)
88
+ else
89
+ super
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end