dry-validation 1.0.0 → 1.5.6

Sign up to get free protection for your applications and to get access to all the features.
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