dry-validation 1.1.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3821e92d2dced61bb892e20a5e5d69b596128fcdbd05596e41fff8716767b08c
4
- data.tar.gz: f797d5e46604e777966cb3d9a7a4032265227684164224a8ff109fc8ba51d1c1
3
+ metadata.gz: 46364c3c3ebe05837ab81f0406c1bbd6d0ad4f6359017dedc92f25627aae6bd4
4
+ data.tar.gz: eaefd86e8b6ce0eebd7140c8eae37f3b40eb7d85400b97cd3e7fa6cf58b8c8af
5
5
  SHA512:
6
- metadata.gz: 7504498b77c794b9fe37e0384fba50a8fb6c99e3fecd3423e66fe018006288b3ba1a5a7a2232fb53dede256ba0a13630313748a0214b4a0d9865fc22c0b9edae
7
- data.tar.gz: 654233d365e7844985f465e16335a234a2d5c2c240b97a485d286a52e6229e3ee80d8719e76d3ec95fe393d75f480617452b7813cb36b4b033021f9b98958461
6
+ metadata.gz: ea2bf0a1bf9d3c451f6d073a6edfcbd6e2b1e33c67a6fa94d3f0c0005155d81fccc6159c9a575dc1b2736a6a00abb094fbd8bb556007f7e980b278404b9e4ed1
7
+ data.tar.gz: be84af9fc46f27e28afc97d7b58dbdf6f031c978d9d94e75a4d2e795a9e0bace3e0db42261259559618291b4dbf637afd8ddeea4ad9f620271dbc7033c5fd212
@@ -1,3 +1,46 @@
1
+ # v1.3.0 2019-08-14
2
+
3
+ ### Added
4
+
5
+ * Support for setting an external schema (that can be extended too) (fixed #574) (@solnic)
6
+
7
+ ### Fixed
8
+
9
+ * Using a hash spec to define rule keys with more than one value is properly handled by rule guard now (fixed #576) (@solnic)
10
+
11
+ ### Changed
12
+
13
+ * `values` within rules uses `Hash#fetch_values` internally now, which improves performance (@esparta)
14
+
15
+ [Compare v1.2.1...v1.3.0](https://github.com/dry-rb/dry-validation/compare/v1.2.1...v1.3.0)
16
+
17
+ # v1.2.1 2019-07-16
18
+
19
+ ### Fixed
20
+
21
+ * Defining an abstract contract class that has no schema no longer crashes (issue #565) (@solnic)
22
+ * Fixed an issue where `Rule#each` would crash when the value is not an array (issue #567) (@solnic)
23
+ * Fixed an issue where guarding a rule would crash when keys are missing in the input (issue #569) (@solnic)
24
+ * Added missing "pathname" require (issue #570) (@solnic)
25
+
26
+ [Compare v1.2.0...v1.2.1](https://github.com/dry-rb/dry-validation/compare/v1.2.0...v1.2.1)
27
+
28
+ # v1.2.0 2019-07-08
29
+
30
+ ### Added
31
+
32
+ * New extension `:predicates_as_macros` (@waiting-for-dev)
33
+
34
+ ### Fixed
35
+
36
+ * Guarding rules for nested keys works correctly (issue #560) (@solnic)
37
+
38
+ ### Changed
39
+
40
+ * `dry-schema` dependency was bumped to `>= 1.3.1` (@solnic)
41
+
42
+ [Compare v1.1.1...v1.2.0](https://github.com/dry-rb/dry-validation/compare/v1.1.1...v1.2.0)
43
+
1
44
  # v1.1.1 2019-06-24
2
45
 
3
46
  ### Fixed
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  [gem]: https://rubygems.org/gems/dry-validation
2
- [travis]: https://travis-ci.org/dry-rb/dry-validation
2
+ [travis]: https://travis-ci.com/dry-rb/dry-validation
3
3
  [codeclimate]: https://codeclimate.com/github/dry-rb/dry-validation
4
4
  [chat]: https://dry-rb.zulipchat.com
5
5
  [inchpages]: http://inch-ci.org/github/dry-rb/dry-validation
@@ -7,7 +7,7 @@
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]
10
+ [![Build Status](https://travis-ci.com/dry-rb/dry-validation.svg?branch=master)][travis]
11
11
  [![Code Climate](https://codeclimate.com/github/dry-rb/dry-validation/badges/gpa.svg)][codeclimate]
12
12
  [![Test Coverage](https://codeclimate.com/github/dry-rb/dry-validation/badges/coverage.svg)][codeclimate]
13
13
  [![Inline docs](http://inch-ci.org/github/dry-rb/dry-validation.svg?branch=master)][inchpages]
@@ -23,6 +23,10 @@ module Dry
23
23
  require 'dry/validation/extensions/hints'
24
24
  end
25
25
 
26
+ register_extension(:predicates_as_macros) do
27
+ require 'dry/validation/extensions/predicates_as_macros'
28
+ end
29
+
26
30
  # Define a contract and build its instance
27
31
  #
28
32
  # @example
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
3
4
  require 'dry/core/constants'
4
5
 
5
6
  module Dry
@@ -116,11 +116,26 @@ module Dry
116
116
  private
117
117
 
118
118
  # @api private
119
- def error?(result, key)
120
- path = Schema::Path[key]
119
+ def error?(result, spec)
120
+ path = Schema::Path[spec]
121
121
 
122
- result.error?(path) ||
123
- path.map.with_index { |_k, i| result.error?(path.keys[0..i - 2]) }.any?
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
+ }
124
139
  end
125
140
 
126
141
  # Get a registered macro
@@ -7,34 +7,9 @@ require 'dry/schema/key_map'
7
7
 
8
8
  require 'dry/validation/constants'
9
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
@@ -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_schema = nil, &block)
61
+ define(:Params, external_schema, &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_schema = nil, &block)
73
+ define(:JSON, external_schema, &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_schema = nil, &block)
85
+ define(:schema, external_schema, &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
@@ -179,12 +154,29 @@ module Dry
179
154
  private
180
155
 
181
156
  # @api private
157
+ # rubocop:disable Metrics/AbcSize
182
158
  def ensure_valid_keys(*keys)
183
159
  valid_paths = key_map.to_dot_notation.map { |value| Schema::Path[value] }
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 = 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)
188
180
 
189
181
  return if invalid_keys.empty?
190
182
 
@@ -192,6 +184,7 @@ module Dry
192
184
  #{name}.rule specifies keys that are not defined by the schema: #{invalid_keys.inspect}
193
185
  STR
194
186
  end
187
+ # rubocop:enable Metrics/AbcSize
195
188
 
196
189
  # @api private
197
190
  def key_map
@@ -199,16 +192,22 @@ module Dry
199
192
  end
200
193
 
201
194
  # @api private
202
- def schema_opts
195
+ def core_schema_opts
203
196
  { parent: superclass&.__schema__, config: config }
204
197
  end
205
198
 
206
199
  # @api private
207
- def define(method_name, &block)
208
- if defined?(@__schema__)
200
+ def define(method_name, external_schema, &block)
201
+ return __schema__ if block.nil?
202
+
203
+ unless __schema__.nil?
209
204
  raise ::Dry::Validation::DuplicateSchemaError, 'Schema has already been defined'
210
205
  end
211
206
 
207
+ schema_opts = core_schema_opts
208
+
209
+ schema_opts.update(parent: external_schema) if external_schema
210
+
212
211
  case method_name
213
212
  when :schema
214
213
  @__schema__ = Schema.define(schema_opts, &block)
@@ -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
@@ -19,21 +19,24 @@ module Dry
19
19
  # @api private
20
20
  option :block
21
21
 
22
+ # @!attribute [r] block_options
23
+ # @return [Hash]
24
+ # @api private
25
+ option :block_options, default: -> { block ? map_keywords(block) : EMPTY_HASH }
26
+
22
27
  private
23
28
 
24
29
  # Extract options for the block kwargs
25
30
  #
26
- # @return [Hash]
31
+ # @param [Proc] block Callable
32
+ # @return Hash
27
33
  #
28
34
  # @api private
29
- def block_options
30
- return EMPTY_HASH unless block
31
-
32
- @block_options ||= block
35
+ def map_keywords(block)
36
+ block
33
37
  .parameters
34
- .select { |arg| arg[0].equal?(:keyreq) }
35
- .map(&:last)
36
- .map { |name| [name, BLOCK_OPTIONS_MAPPINGS[name]] }
38
+ .select { |arg,| arg.equal?(:keyreq) }
39
+ .map { |_, name| [name, BLOCK_OPTIONS_MAPPINGS[name]] }
37
40
  .to_h
38
41
  end
39
42
  end
@@ -106,6 +106,22 @@ module Dry
106
106
  schema_result.error?(key)
107
107
  end
108
108
 
109
+ # Check if there's any error for the provided key
110
+ #
111
+ # This does not consider errors from the nested values
112
+ #
113
+ # @api private
114
+ def base_error?(key)
115
+ schema_result.errors.any? { |error|
116
+ key_path = Schema::Path[key]
117
+ err_path = Schema::Path[error.path]
118
+
119
+ return false unless key_path.same_root?(err_path)
120
+
121
+ key_path == err_path
122
+ }
123
+ end
124
+
109
125
  # Add a new error for the provided key
110
126
  #
111
127
  # @api private
@@ -82,17 +82,21 @@ module Dry
82
82
  @keys = []
83
83
 
84
84
  @block = proc do
85
- (values[root] || []).each_with_index do |_, idx|
86
- path = [*Schema::Path[root].to_a, idx]
85
+ unless result.base_error?(root) || !values.key?(root)
86
+ values[root].each_with_index do |_, idx|
87
+ path = [*Schema::Path[root].to_a, idx]
87
88
 
88
- next if result.error?(path)
89
+ next if result.error?(path)
89
90
 
90
- evaluator = with(macros: macros, keys: [path], &block)
91
+ evaluator = with(macros: macros, keys: [path], &block)
91
92
 
92
- failures.concat(evaluator.failures)
93
+ failures.concat(evaluator.failures)
94
+ end
93
95
  end
94
96
  end
95
97
 
98
+ @block_options = map_keywords(block) if block
99
+
96
100
  self
97
101
  end
98
102
 
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/schema/key'
4
+ require 'dry/schema/key_map'
5
+
6
+ module Dry
7
+ module Schema
8
+ class Path
9
+ # @api private
10
+ def multi_value?
11
+ last.is_a?(Array)
12
+ end
13
+
14
+ # @api private
15
+ def expand
16
+ to_a[0..-2].product(last).map { |spec| self.class[spec] }
17
+ end
18
+ end
19
+
20
+ # @api private
21
+ #
22
+ # TODO: this should be moved to dry-schema at some point
23
+ class Key
24
+ # @api private
25
+ def to_dot_notation
26
+ [name.to_s]
27
+ end
28
+
29
+ # @api private
30
+ class Hash < Key
31
+ # @api private
32
+ def to_dot_notation
33
+ [name].product(members.map(&:to_dot_notation).flatten(1)).map { |e| e.join(DOT) }
34
+ end
35
+ end
36
+ end
37
+
38
+ # @api private
39
+ class KeyMap
40
+ # @api private
41
+ def to_dot_notation
42
+ @to_dot_notation ||= map(&:to_dot_notation).flatten
43
+ end
44
+ end
45
+ end
46
+ end
@@ -45,15 +45,13 @@ module Dry
45
45
 
46
46
  case (key = args[0])
47
47
  when Symbol, String, Array, Hash
48
- path = Schema::Path[key]
49
- keys = path.to_a
48
+ keys = Schema::Path[key].to_a
50
49
 
51
50
  return data.dig(*keys) unless keys.last.is_a?(Array)
52
51
 
53
52
  last = keys.pop
54
53
  vals = self.class.new(data.dig(*keys))
55
-
56
- last.map { |name| vals[name] }
54
+ vals.fetch_values(*last) { nil }
57
55
  else
58
56
  raise ArgumentError, '+key+ must be a valid path specification'
59
57
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module Validation
5
- VERSION = '1.1.1'
5
+ VERSION = '1.3.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-validation
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-06-24 00:00:00.000000000 Z
11
+ date: 2019-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -95,7 +95,7 @@ dependencies:
95
95
  version: '1.0'
96
96
  - - ">="
97
97
  - !ruby/object:Gem::Version
98
- version: 1.1.0
98
+ version: 1.3.1
99
99
  type: :runtime
100
100
  prerelease: false
101
101
  version_requirements: !ruby/object:Gem::Requirement
@@ -105,7 +105,7 @@ dependencies:
105
105
  version: '1.0'
106
106
  - - ">="
107
107
  - !ruby/object:Gem::Version
108
- version: 1.1.0
108
+ version: 1.3.1
109
109
  - !ruby/object:Gem::Dependency
110
110
  name: bundler
111
111
  requirement: !ruby/object:Gem::Requirement
@@ -168,6 +168,7 @@ files:
168
168
  - lib/dry/validation/evaluator.rb
169
169
  - lib/dry/validation/extensions/hints.rb
170
170
  - lib/dry/validation/extensions/monads.rb
171
+ - lib/dry/validation/extensions/predicates_as_macros.rb
171
172
  - lib/dry/validation/failures.rb
172
173
  - lib/dry/validation/function.rb
173
174
  - lib/dry/validation/macro.rb
@@ -177,6 +178,7 @@ files:
177
178
  - lib/dry/validation/messages/resolver.rb
178
179
  - lib/dry/validation/result.rb
179
180
  - lib/dry/validation/rule.rb
181
+ - lib/dry/validation/schema_ext.rb
180
182
  - lib/dry/validation/values.rb
181
183
  - lib/dry/validation/version.rb
182
184
  homepage: https://dry-rb.org/gems/dry-validation