dry-validation 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2019 dry-rb team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,30 @@
1
+ [gem]: https://rubygems.org/gems/dry-validation
2
+ [travis]: https://travis-ci.com/dry-rb/dry-validation
3
+ [codeclimate]: https://codeclimate.com/github/dry-rb/dry-validation
4
+ [chat]: https://dry-rb.zulipchat.com
5
+ [inchpages]: http://inch-ci.org/github/dry-rb/dry-validation
6
+
7
+ # dry-validation [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
8
+
9
+ [![Gem Version](https://badge.fury.io/rb/dry-validation.svg)][gem]
10
+ [![Build Status](https://travis-ci.com/dry-rb/dry-validation.svg?branch=master)][travis]
11
+ [![Code Climate](https://codeclimate.com/github/dry-rb/dry-validation/badges/gpa.svg)][codeclimate]
12
+ [![Test Coverage](https://codeclimate.com/github/dry-rb/dry-validation/badges/coverage.svg)][codeclimate]
13
+ [![Inline docs](http://inch-ci.org/github/dry-rb/dry-validation.svg?branch=master)][inchpages]
14
+
15
+ ## Links
16
+
17
+ * [User documentation](https://dry-rb.org/gems/dry-validation)
18
+ * [API documentation](http://rubydoc.info/gems/dry-validation)
19
+ * [Guidelines for contributing](CONTRIBUTING.md)
20
+
21
+ ## Supported Ruby versions
22
+
23
+ This library officially supports following Ruby versions:
24
+
25
+ * MRI >= `2.4`
26
+ * jruby >= `9.2`
27
+
28
+ ## License
29
+
30
+ See `LICENSE` file.
@@ -0,0 +1,4 @@
1
+ en:
2
+ dry_validation:
3
+ errors:
4
+ acceptance: "must accept %{key}"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/validation'
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/validation/constants'
4
+ require 'dry/validation/contract'
5
+ require 'dry/validation/macros'
6
+
7
+ # Main namespace
8
+ #
9
+ # @api public
10
+ module Dry
11
+ # Main library namespace
12
+ #
13
+ # @api public
14
+ module Validation
15
+ extend Dry::Core::Extensions
16
+ extend Macros::Registrar
17
+
18
+ register_extension(:monads) do
19
+ require 'dry/validation/extensions/monads'
20
+ end
21
+
22
+ register_extension(:hints) do
23
+ require 'dry/validation/extensions/hints'
24
+ end
25
+
26
+ register_extension(:predicates_as_macros) do
27
+ require 'dry/validation/extensions/predicates_as_macros'
28
+ end
29
+
30
+ # Define a contract and build its instance
31
+ #
32
+ # @example
33
+ # my_contract = Dry::Validation.Contract do
34
+ # params do
35
+ # required(:name).filled(:string)
36
+ # end
37
+ # end
38
+ #
39
+ # my_contract.call(name: "Jane")
40
+ #
41
+ # @param [Hash] options Contract options
42
+ #
43
+ # @see Contract
44
+ #
45
+ # @return [Contract]
46
+ #
47
+ # @api public
48
+ #
49
+ # rubocop:disable Naming/MethodName
50
+ def self.Contract(options = EMPTY_HASH, &block)
51
+ Contract.build(options, &block)
52
+ end
53
+ # rubocop:enable Naming/MethodName
54
+
55
+ # This is needed by Macros::Registrar
56
+ #
57
+ # @api private
58
+ def self.macros
59
+ Macros
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/schema/config'
4
+ require 'dry/validation/macros'
5
+
6
+ module Dry
7
+ module Validation
8
+ # Configuration for contracts
9
+ #
10
+ # @see Contract#config
11
+ #
12
+ # @api public
13
+ class Config < Schema::Config
14
+ setting :macros, Macros::Container.new, &:dup
15
+
16
+ # @api private
17
+ def dup
18
+ config = super
19
+ config.macros = macros.dup
20
+ config
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'dry/core/constants'
5
+
6
+ module Dry
7
+ module Validation
8
+ include Dry::Core::Constants
9
+
10
+ DOT = '.'
11
+
12
+ # Root path is used for base errors in hash representation of error messages
13
+ ROOT_PATH = [nil].freeze
14
+
15
+ # Path to the default errors locale file
16
+ DEFAULT_ERRORS_NAMESPACE = 'dry_validation'
17
+
18
+ # Path to the default errors locale file
19
+ DEFAULT_ERRORS_PATH = Pathname(__FILE__).join('../../../../config/errors.yml').realpath.freeze
20
+
21
+ # Mapping for block kwarg options used by block_options
22
+ #
23
+ # @see Rule#block_options
24
+ BLOCK_OPTIONS_MAPPINGS = Hash.new { |_, key| key }.update(context: :_context).freeze
25
+
26
+ # Error raised when `rule` specifies one or more keys that the schema doesn't specify
27
+ InvalidKeysError = Class.new(StandardError)
28
+
29
+ # Error raised when a localized message was not found
30
+ MissingMessageError = Class.new(StandardError)
31
+
32
+ # Error raised when trying to define a schema in a contract class that already has a schema
33
+ DuplicateSchemaError = Class.new(StandardError)
34
+
35
+ # Error raised during initialization of a contract that has no schema defined
36
+ SchemaMissingError = Class.new(StandardError) do
37
+ # @api private
38
+ def initialize(klass)
39
+ super("#{klass} cannot be instantiated without a schema defined")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent/map'
4
+
5
+ require 'dry/equalizer'
6
+ require 'dry/initializer'
7
+ require 'dry/schema/path'
8
+
9
+ require 'dry/validation/config'
10
+ require 'dry/validation/constants'
11
+ require 'dry/validation/rule'
12
+ require 'dry/validation/evaluator'
13
+ require 'dry/validation/messages/resolver'
14
+ require 'dry/validation/result'
15
+ require 'dry/validation/contract/class_interface'
16
+
17
+ module Dry
18
+ module Validation
19
+ # Contract objects apply rules to input
20
+ #
21
+ # A contract consists of a schema and rules. The schema is applied to the
22
+ # input before rules are applied, this way you can be sure that your rules
23
+ # won't be applied to values that didn't pass schema checks.
24
+ #
25
+ # It's up to you how exactly you're going to separate schema checks from
26
+ # your rules.
27
+ #
28
+ # @example
29
+ # class NewUserContract < Dry::Validation::Contract
30
+ # params do
31
+ # required(:email).filled(:string)
32
+ # required(:age).filled(:integer)
33
+ # optional(:login).maybe(:string, :filled?)
34
+ # optional(:password).maybe(:string, min_size?: 10)
35
+ # optional(:password_confirmation).maybe(:string)
36
+ # end
37
+ #
38
+ # rule(:password) do
39
+ # key.failure('is required') if values[:login] && !values[:password]
40
+ # end
41
+ #
42
+ # rule(:age) do
43
+ # key.failure('must be greater or equal 18') if values[:age] < 18
44
+ # end
45
+ # end
46
+ #
47
+ # new_user_contract = NewUserContract.new
48
+ # new_user_contract.call(email: 'jane@doe.org', age: 21)
49
+ #
50
+ # @api public
51
+ class Contract
52
+ include Dry::Equalizer(:schema, :rules, :messages, inspect: false)
53
+
54
+ extend Dry::Initializer
55
+ extend ClassInterface
56
+
57
+ config.messages.top_namespace = DEFAULT_ERRORS_NAMESPACE
58
+ config.messages.load_paths << DEFAULT_ERRORS_PATH
59
+
60
+ # @!attribute [r] config
61
+ # @return [Config] Contract's configuration object
62
+ # @api public
63
+ option :config, default: -> { self.class.config }
64
+
65
+ # @!attribute [r] macros
66
+ # @return [Macros::Container] Configured macros
67
+ # @see Macros::Container#register
68
+ # @api public
69
+ option :macros, default: -> { config.macros }
70
+
71
+ # @!attribute [r] schema
72
+ # @return [Dry::Schema::Params, Dry::Schema::JSON, Dry::Schema::Processor]
73
+ # @api private
74
+ option :schema, default: -> { self.class.__schema__ || raise(SchemaMissingError, self.class) }
75
+
76
+ # @!attribute [r] rules
77
+ # @return [Hash]
78
+ # @api private
79
+ option :rules, default: -> { self.class.rules }
80
+
81
+ # @!attribute [r] message_resolver
82
+ # @return [Messages::Resolver]
83
+ # @api private
84
+ option :message_resolver, default: -> { Messages::Resolver.new(messages) }
85
+
86
+ # Apply the contract to an input
87
+ #
88
+ # @param [Hash] input The input to validate
89
+ #
90
+ # @return [Result]
91
+ #
92
+ # @api public
93
+ def call(input)
94
+ Result.new(schema.(input), Concurrent::Map.new) do |result|
95
+ rules.each do |rule|
96
+ next if rule.keys.any? { |key| error?(result, key) }
97
+
98
+ rule_result = rule.(self, result)
99
+
100
+ rule_result.failures.each do |failure|
101
+ result.add_error(message_resolver[failure])
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ # Return a nice string representation
108
+ #
109
+ # @return [String]
110
+ #
111
+ # @api public
112
+ def inspect
113
+ %(#<#{self.class} schema=#{schema.inspect} rules=#{rules.inspect}>)
114
+ end
115
+
116
+ private
117
+
118
+ # @api private
119
+ def error?(result, spec)
120
+ path = Schema::Path[spec]
121
+
122
+ if path.multi_value?
123
+ return path.expand.any? { |nested_path| error?(result, nested_path) }
124
+ end
125
+
126
+ return true if result.error?(path)
127
+
128
+ path
129
+ .to_a[0..-2]
130
+ .any? { |key|
131
+ curr_path = Schema::Path[path.keys[0..path.keys.index(key)]]
132
+
133
+ return false unless result.error?(curr_path)
134
+
135
+ result.errors.any? { |err|
136
+ (other = Schema::Path[err.path]).same_root?(curr_path) && other == curr_path
137
+ }
138
+ }
139
+ end
140
+
141
+ # Get a registered macro
142
+ #
143
+ # @return [Proc,#to_proc]
144
+ #
145
+ # @api private
146
+ def macro(name, *args)
147
+ (macros.key?(name) ? macros[name] : Macros[name]).with(args)
148
+ end
149
+
150
+ # Return configured messages backend
151
+ #
152
+ # @return [Dry::Schema::Messages::YAML, Dry::Schema::Messages::I18n]
153
+ #
154
+ # @api private
155
+ def messages
156
+ self.class.messages
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/schema'
4
+ require 'dry/schema/messages'
5
+ require 'dry/schema/path'
6
+ require 'dry/schema/key_map'
7
+
8
+ require 'dry/validation/constants'
9
+ require 'dry/validation/macros'
10
+ require 'dry/validation/schema_ext'
11
+
12
+ module Dry
13
+ module Validation
14
+ class Contract
15
+ # Contract's class interface
16
+ #
17
+ # @see Contract
18
+ #
19
+ # @api public
20
+ module ClassInterface
21
+ include Macros::Registrar
22
+
23
+ # @api private
24
+ def inherited(klass)
25
+ super
26
+ klass.instance_variable_set('@config', config.dup)
27
+ end
28
+
29
+ # Configuration
30
+ #
31
+ # @example
32
+ # class MyContract < Dry::Validation::Contract
33
+ # config.messages.backend = :i18n
34
+ # end
35
+ #
36
+ # @return [Config]
37
+ #
38
+ # @api public
39
+ def config
40
+ @config ||= Validation::Config.new
41
+ end
42
+
43
+ # Return macros registered for this class
44
+ #
45
+ # @return [Macros::Container]
46
+ #
47
+ # @api public
48
+ def macros
49
+ config.macros
50
+ end
51
+
52
+ # Define a params schema for your contract
53
+ #
54
+ # This type of schema is suitable for HTTP parameters
55
+ #
56
+ # @return [Dry::Schema::Params,NilClass]
57
+ # @see https://dry-rb.org/gems/dry-schema/params/
58
+ #
59
+ # @api public
60
+ def params(external_schema = nil, &block)
61
+ define(:Params, external_schema, &block)
62
+ end
63
+
64
+ # Define a JSON schema for your contract
65
+ #
66
+ # This type of schema is suitable for JSON data
67
+ #
68
+ # @return [Dry::Schema::JSON,NilClass]
69
+ # @see https://dry-rb.org/gems/dry-schema/json/
70
+ #
71
+ # @api public
72
+ def json(external_schema = nil, &block)
73
+ define(:JSON, external_schema, &block)
74
+ end
75
+
76
+ # Define a plain schema for your contract
77
+ #
78
+ # This type of schema does not offer coercion out of the box
79
+ #
80
+ # @return [Dry::Schema::Processor,NilClass]
81
+ # @see https://dry-rb.org/gems/dry-schema/
82
+ #
83
+ # @api public
84
+ def schema(external_schema = nil, &block)
85
+ define(:schema, external_schema, &block)
86
+ end
87
+
88
+ # Define a rule for your contract
89
+ #
90
+ # @example using a symbol
91
+ # rule(:age) do
92
+ # failure('must be at least 18') if values[:age] < 18
93
+ # end
94
+ #
95
+ # @example using a path to a value and a custom predicate
96
+ # rule('address.street') do
97
+ # failure('please provide a valid street address') if valid_street?(values[:street])
98
+ # end
99
+ #
100
+ # @return [Rule]
101
+ #
102
+ # @api public
103
+ def rule(*keys, &block)
104
+ ensure_valid_keys(*keys) if __schema__
105
+
106
+ Rule.new(keys: keys, block: block).tap do |rule|
107
+ rules << rule
108
+ end
109
+ end
110
+
111
+ # A shortcut that can be used to define contracts that won't be reused or inherited
112
+ #
113
+ # @example
114
+ # my_contract = Dry::Validation::Contract.build do
115
+ # params do
116
+ # required(:name).filled(:string)
117
+ # end
118
+ # end
119
+ #
120
+ # my_contract.call(name: "Jane")
121
+ #
122
+ # @return [Contract]
123
+ #
124
+ # @api public
125
+ def build(options = EMPTY_HASH, &block)
126
+ Class.new(self, &block).new(options)
127
+ end
128
+
129
+ # @api private
130
+ def __schema__
131
+ @__schema__ if defined?(@__schema__)
132
+ end
133
+
134
+ # Return rules defined in this class
135
+ #
136
+ # @return [Array<Rule>]
137
+ #
138
+ # @api private
139
+ def rules
140
+ @rules ||= EMPTY_ARRAY
141
+ .dup
142
+ .concat(superclass.respond_to?(:rules) ? superclass.rules : EMPTY_ARRAY)
143
+ end
144
+
145
+ # Return messages configured for this class
146
+ #
147
+ # @return [Dry::Schema::Messages]
148
+ #
149
+ # @api private
150
+ def messages
151
+ @messages ||= Schema::Messages.setup(config.messages)
152
+ end
153
+
154
+ private
155
+
156
+ # @api private
157
+ # rubocop:disable Metrics/AbcSize
158
+ def ensure_valid_keys(*keys)
159
+ valid_paths = key_map.to_dot_notation.map { |value| Schema::Path[value] }
160
+
161
+ invalid_keys = keys
162
+ .map { |key|
163
+ [key, Schema::Path[key]]
164
+ }
165
+ .map { |(key, path)|
166
+ if (last = path.last).is_a?(Array)
167
+ last.map { |last_key|
168
+ path_key = [*path.to_a[0..-2], last_key]
169
+ [path_key, Schema::Path[path_key]]
170
+ }
171
+ else
172
+ [[key, path]]
173
+ end
174
+ }
175
+ .flatten(1)
176
+ .reject { |(_, path)|
177
+ valid_paths.any? { |valid_path| valid_path.include?(path) }
178
+ }
179
+ .map(&:first)
180
+
181
+ return if invalid_keys.empty?
182
+
183
+ raise InvalidKeysError, <<~STR.strip
184
+ #{name}.rule specifies keys that are not defined by the schema: #{invalid_keys.inspect}
185
+ STR
186
+ end
187
+ # rubocop:enable Metrics/AbcSize
188
+
189
+ # @api private
190
+ def key_map
191
+ __schema__.key_map
192
+ end
193
+
194
+ # @api private
195
+ def core_schema_opts
196
+ { parent: superclass&.__schema__, config: config }
197
+ end
198
+
199
+ # @api private
200
+ def define(method_name, external_schema, &block)
201
+ return __schema__ if external_schema.nil? && block.nil?
202
+
203
+ unless __schema__.nil?
204
+ raise ::Dry::Validation::DuplicateSchemaError, 'Schema has already been defined'
205
+ end
206
+
207
+ schema_opts = core_schema_opts
208
+
209
+ schema_opts.update(parent: external_schema) if external_schema
210
+
211
+ case method_name
212
+ when :schema
213
+ @__schema__ = Schema.define(schema_opts, &block)
214
+ when :Params
215
+ @__schema__ = Schema.Params(schema_opts, &block)
216
+ when :JSON
217
+ @__schema__ = Schema.JSON(schema_opts, &block)
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end