dry-validation 1.0.0 → 1.5.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015-2019 dry-rb team
3
+ Copyright (c) 2015-2020 dry-rb team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -1,26 +1,25 @@
1
1
  [gem]: https://rubygems.org/gems/dry-validation
2
- [travis]: https://travis-ci.org/dry-rb/dry-validation
3
- [codeclimate]: https://codeclimate.com/github/dry-rb/dry-validation
2
+ [actions]: https://github.com/dry-rb/dry-validation/actions
3
+ [codacy]: https://www.codacy.com/gh/dry-rb/dry-validation
4
4
  [chat]: https://dry-rb.zulipchat.com
5
5
  [inchpages]: http://inch-ci.org/github/dry-rb/dry-validation
6
6
 
7
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
8
 
9
9
  [![Gem Version](https://badge.fury.io/rb/dry-validation.svg)][gem]
10
- [![Build Status](https://travis-ci.org/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]
10
+ [![CI Status](https://github.com/dry-rb/dry-validation/workflows/ci/badge.svg)][actions]
11
+ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/f30e3ff5ec304c55a73868cdbf055c67)][codacy]
12
+ [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/f30e3ff5ec304c55a73868cdbf055c67)][codacy]
13
13
  [![Inline docs](http://inch-ci.org/github/dry-rb/dry-validation.svg?branch=master)][inchpages]
14
14
 
15
15
  ## Links
16
16
 
17
- * [User documentation](https://dry-rb.org/gems/dry-validation)
17
+ * [User documentation](http://dry-rb.org/gems/dry-validation)
18
18
  * [API documentation](http://rubydoc.info/gems/dry-validation)
19
- * [Guidelines for contributing](CONTRIBUTING.md)
20
19
 
21
20
  ## Supported Ruby versions
22
21
 
23
- This library officially supports following Ruby versions:
22
+ This library officially supports the following Ruby versions:
24
23
 
25
24
  * MRI >= `2.4`
26
25
  * jruby >= `9.2`
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ # this file is managed by dry-rb/devtools project
3
+
4
+ lib = File.expand_path('lib', __dir__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'dry/validation/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'dry-validation'
10
+ spec.authors = ["Piotr Solnica"]
11
+ spec.email = ["piotr.solnica@gmail.com"]
12
+ spec.license = 'MIT'
13
+ spec.version = Dry::Validation::VERSION.dup
14
+
15
+ spec.summary = "Validation library"
16
+ spec.description = spec.summary
17
+ spec.homepage = 'https://dry-rb.org/gems/dry-validation'
18
+ spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-validation.gemspec", "lib/**/*", "config/*.yml"]
19
+ spec.bindir = 'bin'
20
+ spec.executables = []
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
24
+ spec.metadata['changelog_uri'] = 'https://github.com/dry-rb/dry-validation/blob/master/CHANGELOG.md'
25
+ spec.metadata['source_code_uri'] = 'https://github.com/dry-rb/dry-validation'
26
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/dry-rb/dry-validation/issues'
27
+
28
+ spec.required_ruby_version = ">= 2.4.0"
29
+
30
+ # to update dependencies edit project.yml
31
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
32
+ spec.add_runtime_dependency "dry-container", "~> 0.7", ">= 0.7.1"
33
+ spec.add_runtime_dependency "dry-core", "~> 0.4"
34
+ spec.add_runtime_dependency "dry-equalizer", "~> 0.2"
35
+ spec.add_runtime_dependency "dry-initializer", "~> 3.0"
36
+ spec.add_runtime_dependency "dry-schema", "~> 1.5", ">= 1.5.2"
37
+
38
+ spec.add_development_dependency "bundler"
39
+ spec.add_development_dependency "rake"
40
+ spec.add_development_dependency "rspec"
41
+ end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/validation'
3
+ require "dry/validation"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/validation/constants'
4
- require 'dry/validation/contract'
5
- require 'dry/validation/macros'
3
+ require "dry/validation/constants"
4
+ require "dry/validation/contract"
5
+ require "dry/validation/macros"
6
6
 
7
7
  # Main namespace
8
8
  #
@@ -16,11 +16,15 @@ module Dry
16
16
  extend Macros::Registrar
17
17
 
18
18
  register_extension(:monads) do
19
- require 'dry/validation/extensions/monads'
19
+ require "dry/validation/extensions/monads"
20
20
  end
21
21
 
22
22
  register_extension(:hints) do
23
- require 'dry/validation/extensions/hints'
23
+ require "dry/validation/extensions/hints"
24
+ end
25
+
26
+ register_extension(:predicates_as_macros) do
27
+ require "dry/validation/extensions/predicates_as_macros"
24
28
  end
25
29
 
26
30
  # Define a contract and build its instance
@@ -41,6 +45,7 @@ module Dry
41
45
  # @return [Contract]
42
46
  #
43
47
  # @api public
48
+ #
44
49
  def self.Contract(options = EMPTY_HASH, &block)
45
50
  Contract.build(options, &block)
46
51
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema/config'
4
- require 'dry/validation/macros'
3
+ require "dry/schema/config"
4
+ require "dry/validation/macros"
5
5
 
6
6
  module Dry
7
7
  module Validation
@@ -1,16 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/constants'
3
+ require "pathname"
4
+ require "dry/core/constants"
4
5
 
5
6
  module Dry
6
7
  module Validation
7
8
  include Dry::Core::Constants
8
9
 
9
- DOT = '.'
10
+ DOT = "."
10
11
 
11
12
  # Root path is used for base errors in hash representation of error messages
12
13
  ROOT_PATH = [nil].freeze
13
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
+
14
21
  # Mapping for block kwarg options used by block_options
15
22
  #
16
23
  # @see Rule#block_options
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent/map'
3
+ require "concurrent/map"
4
4
 
5
- require 'dry/equalizer'
6
- require 'dry/initializer'
7
- require 'dry/schema/path'
5
+ require "dry/equalizer"
6
+ require "dry/initializer"
7
+ require "dry/schema/path"
8
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'
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
16
 
17
17
  module Dry
18
18
  module Validation
@@ -54,8 +54,8 @@ module Dry
54
54
  extend Dry::Initializer
55
55
  extend ClassInterface
56
56
 
57
- config.messages.top_namespace = 'dry_validation'
58
- config.messages.load_paths << Pathname(__FILE__).join('../../../../config/errors.yml').realpath
57
+ config.messages.top_namespace = DEFAULT_ERRORS_NAMESPACE
58
+ config.messages.load_paths << DEFAULT_ERRORS_PATH
59
59
 
60
60
  # @!attribute [r] config
61
61
  # @return [Config] Contract's configuration object
@@ -98,7 +98,7 @@ module Dry
98
98
  rule_result = rule.(self, result)
99
99
 
100
100
  rule_result.failures.each do |failure|
101
- result.add_error(message_resolver[failure])
101
+ result.add_error(message_resolver.(**failure))
102
102
  end
103
103
  end
104
104
  end
@@ -116,9 +116,26 @@ module Dry
116
116
  private
117
117
 
118
118
  # @api private
119
- def error?(result, key)
120
- path = Schema::Path[key]
121
- result.error?(path) || path.map.with_index { |_k, i| result.error?(path.keys[0..i - 2]) }.any?
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.schema_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.schema_error?(curr_path)
134
+
135
+ result.errors.any? { |err|
136
+ (other = Schema::Path[err.path]).same_root?(curr_path) && other == curr_path
137
+ }
138
+ }
122
139
  end
123
140
 
124
141
  # Get a registered macro
@@ -1,40 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/schema'
4
- require 'dry/schema/messages'
5
- require 'dry/schema/path'
6
- require 'dry/schema/key_map'
3
+ require "dry/schema"
4
+ require "dry/schema/messages"
5
+ require "dry/schema/path"
6
+ require "dry/schema/key_map"
7
7
 
8
- require 'dry/validation/constants'
9
- require 'dry/validation/macros'
8
+ require "dry/validation/constants"
9
+ require "dry/validation/macros"
10
+ require "dry/validation/schema_ext"
10
11
 
11
12
  module Dry
12
- module Schema
13
- # @api private
14
- class Key
15
- # @api private
16
- def to_dot_notation
17
- [name.to_s]
18
- end
19
-
20
- # @api private
21
- class Hash < Key
22
- # @api private
23
- def to_dot_notation
24
- [name].product(members.map(&:to_dot_notation).flatten(1)).map { |e| e.join(DOT) }
25
- end
26
- end
27
- end
28
-
29
- # @api private
30
- class KeyMap
31
- # @api private
32
- def to_dot_notation
33
- @to_dot_notation ||= map(&:to_dot_notation).flatten
34
- end
35
- end
36
- end
37
-
38
13
  module Validation
39
14
  class Contract
40
15
  # Contract's class interface
@@ -48,7 +23,7 @@ module Dry
48
23
  # @api private
49
24
  def inherited(klass)
50
25
  super
51
- klass.instance_variable_set('@config', config.dup)
26
+ klass.instance_variable_set("@config", config.dup)
52
27
  end
53
28
 
54
29
  # Configuration
@@ -78,36 +53,36 @@ module Dry
78
53
  #
79
54
  # This type of schema is suitable for HTTP parameters
80
55
  #
81
- # @return [Dry::Schema::Params]
56
+ # @return [Dry::Schema::Params,NilClass]
82
57
  # @see https://dry-rb.org/gems/dry-schema/params/
83
58
  #
84
59
  # @api public
85
- def params(&block)
86
- define(:Params, &block)
60
+ def params(*external_schemas, &block)
61
+ define(:Params, external_schemas, &block)
87
62
  end
88
63
 
89
64
  # Define a JSON schema for your contract
90
65
  #
91
66
  # This type of schema is suitable for JSON data
92
67
  #
93
- # @return [Dry::Schema::JSON]
68
+ # @return [Dry::Schema::JSON,NilClass]
94
69
  # @see https://dry-rb.org/gems/dry-schema/json/
95
70
  #
96
71
  # @api public
97
- def json(&block)
98
- define(:JSON, &block)
72
+ def json(*external_schemas, &block)
73
+ define(:JSON, external_schemas, &block)
99
74
  end
100
75
 
101
76
  # Define a plain schema for your contract
102
77
  #
103
78
  # This type of schema does not offer coercion out of the box
104
79
  #
105
- # @return [Dry::Schema::Processor]
80
+ # @return [Dry::Schema::Processor,NilClass]
106
81
  # @see https://dry-rb.org/gems/dry-schema/
107
82
  #
108
83
  # @api public
109
- def schema(&block)
110
- define(:schema, &block)
84
+ def schema(*external_schemas, &block)
85
+ define(:schema, external_schemas, &block)
111
86
  end
112
87
 
113
88
  # Define a rule for your contract
@@ -126,7 +101,7 @@ module Dry
126
101
  #
127
102
  # @api public
128
103
  def rule(*keys, &block)
129
- ensure_valid_keys(*keys)
104
+ ensure_valid_keys(*keys) if __schema__
130
105
 
131
106
  Rule.new(keys: keys, block: block).tap do |rule|
132
107
  rules << rule
@@ -148,7 +123,7 @@ module Dry
148
123
  #
149
124
  # @api public
150
125
  def build(options = EMPTY_HASH, &block)
151
- Class.new(self, &block).new(options)
126
+ Class.new(self, &block).new(**options)
152
127
  end
153
128
 
154
129
  # @api private
@@ -180,11 +155,14 @@ module Dry
180
155
 
181
156
  # @api private
182
157
  def ensure_valid_keys(*keys)
183
- valid_paths = key_map.to_dot_notation.map { |value| Schema::Path[value] }
158
+ valid_paths = key_map.to_dot_notation
159
+ key_paths = key_paths(keys)
184
160
 
185
- invalid_keys = Schema::KeyMap[*keys]
186
- .map(&:dump)
187
- .reject { |spec| valid_paths.any? { |path| path.include?(Schema::Path[spec]) } }
161
+ invalid_keys = key_paths.map { |(key, path)|
162
+ unless valid_paths.any? { |vp| vp.include?(path) || vp.include?("#{path}[]") }
163
+ key
164
+ end
165
+ }.compact.uniq
188
166
 
189
167
  return if invalid_keys.empty?
190
168
 
@@ -193,29 +171,57 @@ module Dry
193
171
  STR
194
172
  end
195
173
 
174
+ # @api private
175
+ def key_paths(keys)
176
+ keys.map { |key|
177
+ case key
178
+ when Hash
179
+ path = Schema::Path[key]
180
+ if path.multi_value?
181
+ *head, tail = Array(path)
182
+ [key].product(
183
+ tail.map { |el| [*head, *el] }.map { |parts| parts.join(DOT) }
184
+ )
185
+ else
186
+ [[key, path.to_a.join(DOT)]]
187
+ end
188
+ when Array
189
+ [[key, Schema::Path[key].to_a.join(DOT)]]
190
+ else
191
+ [[key, key.to_s]]
192
+ end
193
+ }.flatten(1)
194
+ end
195
+
196
196
  # @api private
197
197
  def key_map
198
198
  __schema__.key_map
199
199
  end
200
200
 
201
201
  # @api private
202
- def schema_opts
203
- { parent: superclass&.__schema__, config: config }
202
+ def core_schema_opts
203
+ {parent: superclass&.__schema__, config: config}
204
204
  end
205
205
 
206
206
  # @api private
207
- def define(method_name, &block)
208
- if defined?(@__schema__)
209
- raise ::Dry::Validation::DuplicateSchemaError, 'Schema has already been defined'
207
+ def define(method_name, external_schemas, &block)
208
+ return __schema__ if external_schemas.empty? && block.nil?
209
+
210
+ unless __schema__.nil?
211
+ raise ::Dry::Validation::DuplicateSchemaError, "Schema has already been defined"
210
212
  end
211
213
 
214
+ schema_opts = core_schema_opts
215
+
216
+ schema_opts.update(parent: external_schemas) if external_schemas.any?
217
+
212
218
  case method_name
213
219
  when :schema
214
- @__schema__ = Schema.define(schema_opts, &block)
220
+ @__schema__ = Schema.define(**schema_opts, &block)
215
221
  when :Params
216
- @__schema__ = Schema.Params(schema_opts, &block)
222
+ @__schema__ = Schema.Params(**schema_opts, &block)
217
223
  when :JSON
218
- @__schema__ = Schema.JSON(schema_opts, &block)
224
+ @__schema__ = Schema.JSON(**schema_opts, &block)
219
225
  end
220
226
  end
221
227
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/initializer'
3
+ require "dry/initializer"
4
+ require "dry/core/deprecations"
4
5
 
5
- require 'dry/validation/constants'
6
- require 'dry/validation/failures'
6
+ require "dry/validation/constants"
7
+ require "dry/validation/failures"
7
8
 
8
9
  module Dry
9
10
  module Validation
@@ -16,6 +17,9 @@ module Dry
16
17
  # @api public
17
18
  class Evaluator
18
19
  extend Dry::Initializer
20
+ extend Dry::Core::Deprecations[:'dry-validation']
21
+
22
+ deprecate :error?, :schema_error?
19
23
 
20
24
  # @!attribute [r] _contract
21
25
  # @return [Contract]
@@ -63,19 +67,19 @@ module Dry
63
67
  # Initialize a new evaluator
64
68
  #
65
69
  # @api private
66
- def initialize(contract, options, &block)
67
- super(contract, options)
70
+ def initialize(contract, **options, &block)
71
+ super(contract, **options)
68
72
 
69
73
  @_options = options
70
74
 
71
75
  if block
72
76
  exec_opts = block_options.map { |key, value| [key, _options[value]] }.to_h
73
- instance_exec(exec_opts, &block)
77
+ instance_exec(**exec_opts, &block)
74
78
  end
75
79
 
76
80
  macros.each do |args|
77
81
  macro = macro(*args.flatten(1))
78
- instance_exec(macro.extract_block_options(_options.merge(macro: macro)), &macro.block)
82
+ instance_exec(**macro.extract_block_options(_options.merge(macro: macro)), &macro.block)
79
83
  end
80
84
  end
81
85
 
@@ -117,7 +121,7 @@ module Dry
117
121
 
118
122
  # @api private
119
123
  def with(new_opts, &block)
120
- self.class.new(_contract, _options.merge(new_opts), &block)
124
+ self.class.new(_contract, **_options, **new_opts, &block)
121
125
  end
122
126
 
123
127
  # Return default (first) key name
@@ -142,20 +146,52 @@ module Dry
142
146
  #
143
147
  # @return [Object]
144
148
  #
145
- # @public
149
+ # @api public
146
150
  def value
147
151
  values[key_name]
148
152
  end
149
153
 
150
- # Check if there are any errors under the provided path
154
+ # Return if the value under the default key is available
155
+ #
156
+ # This is useful when dealing with rules for optional keys
157
+ #
158
+ # @example use the default key name
159
+ # rule(:age) do
160
+ # key.failure(:invalid) if key? && value < 18
161
+ # end
162
+ #
163
+ # @example specify the key name
164
+ # rule(:start_date, :end_date) do
165
+ # if key?(:start_date) && !key?(:end_date)
166
+ # key(:end_date).failure("must provide an end_date with start_date")
167
+ # end
168
+ # end
169
+ #
170
+ # @return [Boolean]
171
+ #
172
+ # @api public
173
+ def key?(name = key_name)
174
+ values.key?(name)
175
+ end
176
+
177
+ # Check if there are any errors on the schema under the provided path
178
+ #
179
+ # @param path [Symbol, String, Array] A Path-compatible spec
151
180
  #
152
- # @param [Symbol, String, Array] A Path-compatible spec
181
+ # @return [Boolean]
182
+ #
183
+ # @api public
184
+ def schema_error?(path)
185
+ result.schema_error?(path)
186
+ end
187
+
188
+ # Check if there are any errors on the current rule
153
189
  #
154
190
  # @return [Boolean]
155
191
  #
156
192
  # @api public
157
- def error?(path)
158
- result.error?(path)
193
+ def rule_error?
194
+ !key(path).empty?
159
195
  end
160
196
 
161
197
  # @api private
@@ -176,6 +212,7 @@ module Dry
176
212
  super
177
213
  end
178
214
  end
215
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
179
216
  end
180
217
  end
181
218
  end