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