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,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