dry-validation 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/initializer'
4
+
5
+ require 'dry/validation/constants'
6
+ require 'dry/validation/failures'
7
+
8
+ module Dry
9
+ module Validation
10
+ # Evaluator is the execution context for rules
11
+ #
12
+ # Evaluators expose an API for setting failure messages and forward
13
+ # method calls to the contracts, so that you can use your contract
14
+ # methods within rule blocks
15
+ #
16
+ # @api public
17
+ class Evaluator
18
+ extend Dry::Initializer
19
+
20
+ # @!attribute [r] _contract
21
+ # @return [Contract]
22
+ # @api private
23
+ param :_contract
24
+
25
+ # @!attribute [r] result
26
+ # @return [Result]
27
+ # @api private
28
+ option :result
29
+
30
+ # @!attribute [r] keys
31
+ # @return [Array<String, Symbol, Hash>]
32
+ # @api private
33
+ option :keys
34
+
35
+ # @!attribute [r] macros
36
+ # @return [Array<Symbol>]
37
+ # @api private
38
+ option :macros, optional: true, default: proc { EMPTY_ARRAY.dup }
39
+
40
+ # @!attribute [r] _context
41
+ # @return [Concurrent::Map]
42
+ # @api private
43
+ option :_context
44
+
45
+ # @!attribute [r] path
46
+ # @return [Dry::Schema::Path]
47
+ # @api private
48
+ option :path, default: proc { Dry::Schema::Path[(key = keys.first) ? key : ROOT_PATH] }
49
+
50
+ # @!attribute [r] values
51
+ # @return [Object]
52
+ # @api private
53
+ option :values
54
+
55
+ # @!attribute [r] block_options
56
+ # @return [Hash<Symbol=>Symbol>]
57
+ # @api private
58
+ option :block_options, default: proc { EMPTY_HASH }
59
+
60
+ # @return [Hash]
61
+ attr_reader :_options
62
+
63
+ # Initialize a new evaluator
64
+ #
65
+ # @api private
66
+ def initialize(contract, options, &block)
67
+ super(contract, options)
68
+
69
+ @_options = options
70
+
71
+ if block
72
+ exec_opts = block_options.map { |key, value| [key, _options[value]] }.to_h
73
+ instance_exec(exec_opts, &block)
74
+ end
75
+
76
+ macros.each do |args|
77
+ macro = macro(*args.flatten(1))
78
+ instance_exec(macro.extract_block_options(_options.merge(macro: macro)), &macro.block)
79
+ end
80
+ end
81
+
82
+ # Get `Failures` object for the default or provided path
83
+ #
84
+ # @param [Symbol,String,Hash,Array<Symbol>] path
85
+ #
86
+ # @return [Failures]
87
+ #
88
+ # @see Failures#failure
89
+ #
90
+ # @api public
91
+ def key(path = self.path)
92
+ (@key ||= EMPTY_HASH.dup)[path] ||= Failures.new(path)
93
+ end
94
+
95
+ # Get `Failures` object for base errors
96
+ #
97
+ # @return [Failures]
98
+ #
99
+ # @see Failures#failure
100
+ #
101
+ # @api public
102
+ def base
103
+ @base ||= Failures.new
104
+ end
105
+
106
+ # Return aggregated failures
107
+ #
108
+ # @return [Array<Hash>]
109
+ #
110
+ # @api private
111
+ def failures
112
+ @failures ||= []
113
+ @failures += @base.opts if defined?(@base)
114
+ @failures.concat(@key.values.flat_map(&:opts)) if defined?(@key)
115
+ @failures
116
+ end
117
+
118
+ # @api private
119
+ def with(new_opts, &block)
120
+ self.class.new(_contract, _options.merge(new_opts), &block)
121
+ end
122
+
123
+ # Return default (first) key name
124
+ #
125
+ # @return [Symbol]
126
+ #
127
+ # @api public
128
+ def key_name
129
+ @key_name ||= keys.first
130
+ end
131
+
132
+ # Return the value found under the first specified key
133
+ #
134
+ # This is a convenient method that can be used in all the common cases
135
+ # where a rule depends on just one key and you want a quick access to
136
+ # the value
137
+ #
138
+ # @example
139
+ # rule(:age) do
140
+ # key.failure(:invalid) if value < 18
141
+ # end
142
+ #
143
+ # @return [Object]
144
+ #
145
+ # @public
146
+ def value
147
+ values[key_name]
148
+ end
149
+
150
+ # Return if the value under the default key is available
151
+ #
152
+ # This is useful when dealing with rules for optional keys
153
+ #
154
+ # @example
155
+ # rule(:age) do
156
+ # key.failure(:invalid) if key? && value < 18
157
+ # end
158
+ #
159
+ # @return [Boolean]
160
+ #
161
+ # @api public
162
+ def key?
163
+ values.key?(key_name)
164
+ end
165
+
166
+ # Check if there are any errors under the provided path
167
+ #
168
+ # @param [Symbol, String, Array] A Path-compatible spec
169
+ #
170
+ # @return [Boolean]
171
+ #
172
+ # @api public
173
+ def error?(path)
174
+ result.error?(path)
175
+ end
176
+
177
+ # @api private
178
+ def respond_to_missing?(meth, include_private = false)
179
+ super || _contract.respond_to?(meth, true)
180
+ end
181
+
182
+ private
183
+
184
+ # Forward to the underlying contract
185
+ #
186
+ # @api private
187
+ def method_missing(meth, *args, &block)
188
+ # yes, we do want to delegate to private methods too
189
+ if _contract.respond_to?(meth, true)
190
+ _contract.__send__(meth, *args, &block)
191
+ else
192
+ super
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/monads/result'
4
+
5
+ module Dry
6
+ module Validation
7
+ # Hints extension
8
+ #
9
+ # @example
10
+ # Dry::Validation.load_extensions(:hints)
11
+ #
12
+ # contract = Dry::Validation::Contract.build do
13
+ # schema do
14
+ # required(:name).filled(:string, min_size?: 2..4)
15
+ # end
16
+ # end
17
+ #
18
+ # contract.call(name: "fo").hints
19
+ # # {:name=>["size must be within 2 - 4"]}
20
+ #
21
+ # contract.call(name: "").messages
22
+ # # {:name=>["must be filled", "size must be within 2 - 4"]}
23
+ #
24
+ # @api public
25
+ module Hints
26
+ # Hints extensions for Result
27
+ #
28
+ # @api public
29
+ module ResultExtensions
30
+ # Return error messages excluding hints
31
+ #
32
+ # @macro errors-options
33
+ # @return [MessageSet]
34
+ #
35
+ # @api public
36
+ def errors(new_options = EMPTY_HASH)
37
+ opts = new_options.merge(hints: false)
38
+ @errors.with(schema_errors(opts), opts)
39
+ end
40
+
41
+ # Return errors and hints
42
+ #
43
+ # @macro errors-options
44
+ #
45
+ # @return [MessageSet]
46
+ #
47
+ # @api public
48
+ def messages(new_options = EMPTY_HASH)
49
+ errors.with(hints.to_a, options.merge(**new_options))
50
+ end
51
+
52
+ # Return hint messages
53
+ #
54
+ # @macro errors-options
55
+ #
56
+ # @return [MessageSet]
57
+ #
58
+ # @api public
59
+ def hints(new_options = EMPTY_HASH)
60
+ schema_result.hints(new_options)
61
+ end
62
+ end
63
+
64
+ Dry::Schema.load_extensions(:hints)
65
+
66
+ Result.prepend(ResultExtensions)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/monads/result'
4
+
5
+ module Dry
6
+ module Validation
7
+ # Monad extension for contract results
8
+ #
9
+ # @example
10
+ # Dry::Validation.load_extensions(:monads)
11
+ #
12
+ # contract = Dry::Validation::Contract.build do
13
+ # schema do
14
+ # required(:name).filled(:string)
15
+ # end
16
+ # end
17
+ #
18
+ # contract.call(name: nil).to_monad
19
+ #
20
+ # @api public
21
+ class Result
22
+ include Dry::Monads::Result::Mixin
23
+
24
+ # Returns a result monad
25
+ #
26
+ # @return [Dry::Monads::Result]
27
+ #
28
+ # @api public
29
+ def to_monad
30
+ success? ? Success(self) : Failure(self)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/schema/predicate_registry'
4
+ require 'dry/validation/contract'
5
+
6
+ module Dry
7
+ module Validation
8
+ # Predicate registry with additional needed methods.
9
+ class PredicateRegistry < Schema::PredicateRegistry
10
+ # List of predicates to be imported by `:predicates_as_macros`
11
+ # extension.
12
+ #
13
+ # @see Dry::Validation::Contract
14
+ WHITELIST = %i[
15
+ filled? gt? gteq? included_in? includes? inclusion? is? lt?
16
+ lteq? max_size? min_size? not_eql? odd? respond_to? size? true?
17
+ uuid_v4?
18
+ ].freeze
19
+
20
+ # @api private
21
+ def arg_names(name)
22
+ arg_list(name).map(&:first)
23
+ end
24
+
25
+ # @api private
26
+ def call(name, args)
27
+ self[name].(*args)
28
+ end
29
+
30
+ # @api private
31
+ def message_opts(name, arg_values)
32
+ arg_names(name).zip(arg_values).to_h
33
+ end
34
+ end
35
+
36
+ # Extension to use dry-logic predicates as macros.
37
+ #
38
+ # @see Dry::Validation::PredicateRegistry::WHITELIST Available predicates
39
+ #
40
+ # @example
41
+ # Dry::Validation.load_extensions(:predicates_as_macros)
42
+ #
43
+ # class ApplicationContract < Dry::Validation::Contract
44
+ # import_predicates_as_macros
45
+ # end
46
+ #
47
+ # class AgeContract < ApplicationContract
48
+ # schema do
49
+ # required(:age).filled(:integer)
50
+ # end
51
+ #
52
+ # rule(:age).validate(gteq?: 18)
53
+ # end
54
+ #
55
+ # AgeContract.new.(age: 17).errors.first.text
56
+ # # => 'must be greater than or equal to 18'
57
+ #
58
+ # @api public
59
+ class Contract
60
+ # Make macros available for self and its descendants.
61
+ def self.import_predicates_as_macros
62
+ registry = PredicateRegistry.new
63
+
64
+ PredicateRegistry::WHITELIST.each do |name|
65
+ register_macro(name) do |macro:|
66
+ predicate_args = [*macro.args, value]
67
+ message_opts = registry.message_opts(name, predicate_args)
68
+
69
+ key.failure(name, message_opts) unless registry.(name, predicate_args)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/schema/path'
4
+ require 'dry/validation/constants'
5
+
6
+ module Dry
7
+ module Validation
8
+ # Failure accumulator object
9
+ #
10
+ # @api public
11
+ class Failures
12
+ # The path for messages accumulated by failures object
13
+ #
14
+ # @return [Dry::Schema::Path]
15
+ #
16
+ # @api private
17
+ attr_reader :path
18
+
19
+ # Options for messages
20
+ #
21
+ # These options are used by MessageResolver
22
+ #
23
+ # @return [Hash]
24
+ #
25
+ # @api private
26
+ attr_reader :opts
27
+
28
+ # @api private
29
+ def initialize(path = ROOT_PATH)
30
+ @path = Dry::Schema::Path[path]
31
+ @opts = EMPTY_ARRAY.dup
32
+ end
33
+
34
+ # Set failure
35
+ #
36
+ # @overload failure(message)
37
+ # Set message text explicitly
38
+ # @param message [String] The message text
39
+ # @example
40
+ # failure('this failed')
41
+ #
42
+ # @overload failure(id)
43
+ # Use message identifier (needs localized messages setup)
44
+ # @param id [Symbol] The message id
45
+ # @example
46
+ # failure(:taken)
47
+ #
48
+ # @see Evaluator#key
49
+ # @see Evaluator#base
50
+ #
51
+ # @api public
52
+ def failure(message, tokens = EMPTY_HASH)
53
+ opts << { message: message, tokens: tokens, path: path }
54
+ self
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/initializer'
4
+ require 'dry/validation/constants'
5
+
6
+ module Dry
7
+ module Validation
8
+ # Abstract class for handling rule blocks
9
+ #
10
+ # @see Rule
11
+ # @see Macro
12
+ #
13
+ # @api private
14
+ class Function
15
+ extend Dry::Initializer
16
+
17
+ # @!attribute [r] block
18
+ # @return [Proc]
19
+ # @api private
20
+ option :block
21
+
22
+ # @!attribute [r] block_options
23
+ # @return [Hash]
24
+ # @api private
25
+ option :block_options, default: -> { block ? map_keywords(block) : EMPTY_HASH }
26
+
27
+ private
28
+
29
+ # Extract options for the block kwargs
30
+ #
31
+ # @param [Proc] block Callable
32
+ # @return Hash
33
+ #
34
+ # @api private
35
+ def map_keywords(block)
36
+ block
37
+ .parameters
38
+ .select { |arg,| arg.equal?(:keyreq) }
39
+ .map { |_, name| [name, BLOCK_OPTIONS_MAPPINGS[name]] }
40
+ .to_h
41
+ end
42
+ end
43
+ end
44
+ end