dry-validation 1.0.0.rc3 → 1.0.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/lib/dry/validation.rb +8 -17
- data/lib/dry/validation/config.rb +4 -8
- data/lib/dry/validation/constants.rb +19 -0
- data/lib/dry/validation/contract.rb +18 -12
- data/lib/dry/validation/contract/class_interface.rb +53 -21
- data/lib/dry/validation/evaluator.rb +64 -48
- data/lib/dry/validation/failures.rb +58 -0
- data/lib/dry/validation/function.rb +41 -0
- data/lib/dry/validation/macro.rb +38 -0
- data/lib/dry/validation/macros.rb +35 -4
- data/lib/dry/validation/message_set.rb +2 -2
- data/lib/dry/validation/messages/resolver.rb +3 -9
- data/lib/dry/validation/rule.rb +41 -11
- data/lib/dry/validation/values.rb +13 -2
- data/lib/dry/validation/version.rb +1 -1
- metadata +33 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7acb8c840fe5a4c12564c86985078e25cff1bea877b9e2dc4f29b39bc892b6a5
|
4
|
+
data.tar.gz: ae09dc37911b0e4031918f22efabd53035501119a71da60ce08b4e0a00a3e0b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9430d9ca1660c3d280dd5c32f1787bce32c06c38ecd22987f53ccce8b581debce2679b95dc3b2eb9a96598d16c2dc0d99f5b65aa26de14fe0f2a03c5bb822a87
|
7
|
+
data.tar.gz: a07ad85054ae898cd155db7874219332e58956f57904c1dde445d27d34aa81aa2b7d1a431e6218f6aa320bfdb0e0fffd20fd619a98991929b12c1f643326c77a
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,42 @@
|
|
1
|
+
# v1.0.0 2019-06-10
|
2
|
+
|
3
|
+
This release is a complete rewrite on top of `dry-schema` that uses contract classes to define schema and validation rules. It's **not backward-compatible**. This release addressed over 150 known issues, including bugs and missing features.
|
4
|
+
|
5
|
+
See [the list of all addressed issues](https://github.com/dry-rb/dry-validation/issues?utf8=✓&q=is%3Aissue+is%3Aclosed+closed%3A%3E%3D2019-01-01+) as well as issues that were moved to dry-schema and [addressed there](https://github.com/dry-rb/dry-schema/issues?q=is%3Aissue+is%3Aclosed+dry-validation+milestone%3A1.0.0).
|
6
|
+
|
7
|
+
### :sparkles: Release highlights :sparkles:
|
8
|
+
|
9
|
+
- New `Dry::Validation::Contract` API for defining contract classes with schemas and validation rules
|
10
|
+
- Improved message backends with support for `key` and `base` messages, and arbitrary meta-data (like error codes etc.)
|
11
|
+
- Support for defining rules for array elements ie `rule(:items).each { ... }`
|
12
|
+
- Support for macros that encapsulate common rule logic
|
13
|
+
- Built-in `:acceptance` macro
|
14
|
+
|
15
|
+
[Compare v0.13.3...v1.0.0](https://github.com/dry-rb/dry-validation/compare/v1.13.3...v1.0.0)
|
16
|
+
|
17
|
+
# v1.0.0 2019-06-10 (compared to 1.0.0.rc3)
|
18
|
+
|
19
|
+
### Added
|
20
|
+
|
21
|
+
- Support for defining rules for each element of an array via `rule(:items).each { ... }` (solnic)
|
22
|
+
- Support for parameterized macros via `rule(:foo).validate(my_macro: :some_option)` (solnic)
|
23
|
+
- `values#[]` is now compatible with path specs (symbol, array with keys or dot-notation) (issue #528) (solnic)
|
24
|
+
- `value` shortcut for accessing the value found under the first key specified by a rule. ie `rule(:foo) { value }` returns `values[:foo]` (solnic)
|
25
|
+
|
26
|
+
### Fixed
|
27
|
+
|
28
|
+
- Contract's `config.locale` option was replaced by `config.messages.default_locale` to avoid conflicts with run-time `:locale` option and/or whatever is set via `I18n` gem (solnic)
|
29
|
+
- Macros no longer mutate `Dry::Validation::Contract.macros` when using inheritance (solnic)
|
30
|
+
- Missing dependency on `dry-container` was added (solnic)
|
31
|
+
|
32
|
+
### Changed
|
33
|
+
|
34
|
+
- `rule` will raise `InvalidKeysError` when specified keys are not defined by the schema (solnic)
|
35
|
+
- `Contract.new` will raise `SchemaMissingError` when the class doesn't have schema defined (solnic)
|
36
|
+
- Contracts no longer support `:locale` option in the constructor. Use `Result#errors(locale: :pl)` to change locale at run-time (solnic)
|
37
|
+
|
38
|
+
[Compare v1.0.0.rc3...v1.0.0](https://github.com/dry-rb/dry-validation/compare/v1.0.0.rc3...v1.0.0)
|
39
|
+
|
1
40
|
# v1.0.0.rc3 2019-05-06
|
2
41
|
|
3
42
|
### Added
|
@@ -15,6 +54,8 @@
|
|
15
54
|
|
16
55
|
# v1.0.0.rc2 2019-05-04
|
17
56
|
|
57
|
+
This was **yanked** on rubygems.org because the bundled gem was missing `config` directory, thus it was not possible to require it. It was fixed in `rc3`.
|
58
|
+
|
18
59
|
### Added
|
19
60
|
|
20
61
|
* [EXPERIMENTAL] support for registering macros via `Dry::Validation::Macros.register(:your_macro, &block)` (solnic)
|
data/lib/dry/validation.rb
CHANGED
@@ -13,6 +13,7 @@ module Dry
|
|
13
13
|
# @api public
|
14
14
|
module Validation
|
15
15
|
extend Dry::Core::Extensions
|
16
|
+
extend Macros::Registrar
|
16
17
|
|
17
18
|
register_extension(:monads) do
|
18
19
|
require 'dry/validation/extensions/monads'
|
@@ -22,23 +23,6 @@ module Dry
|
|
22
23
|
require 'dry/validation/extensions/hints'
|
23
24
|
end
|
24
25
|
|
25
|
-
# Register a new global macro
|
26
|
-
#
|
27
|
-
# @example
|
28
|
-
# Dry::Validation.register_macro(:even_numbers) do
|
29
|
-
# key.failure('all numbers must be even') unless values[key_name].all?(&:even?)
|
30
|
-
# end
|
31
|
-
#
|
32
|
-
# @param [Symbol] name The name of the macro
|
33
|
-
#
|
34
|
-
# @return [self]
|
35
|
-
#
|
36
|
-
# @api public
|
37
|
-
def self.register_macro(name, &block)
|
38
|
-
Macros.register(name, &block)
|
39
|
-
self
|
40
|
-
end
|
41
|
-
|
42
26
|
# Define a contract and build its instance
|
43
27
|
#
|
44
28
|
# @example
|
@@ -60,5 +44,12 @@ module Dry
|
|
60
44
|
def self.Contract(options = EMPTY_HASH, &block)
|
61
45
|
Contract.build(options, &block)
|
62
46
|
end
|
47
|
+
|
48
|
+
# This is needed by Macros::Registrar
|
49
|
+
#
|
50
|
+
# @api private
|
51
|
+
def self.macros
|
52
|
+
Macros
|
53
|
+
end
|
63
54
|
end
|
64
55
|
end
|
@@ -11,17 +11,13 @@ module Dry
|
|
11
11
|
#
|
12
12
|
# @api public
|
13
13
|
class Config < Schema::Config
|
14
|
-
setting :locale, :en
|
15
14
|
setting :macros, Macros::Container.new, &:dup
|
16
15
|
|
17
16
|
# @api private
|
18
|
-
def
|
19
|
-
config
|
20
|
-
|
21
|
-
|
22
|
-
# @api private
|
23
|
-
def locale
|
24
|
-
config.locale
|
17
|
+
def dup
|
18
|
+
config = super
|
19
|
+
config.macros = macros.dup
|
20
|
+
config
|
25
21
|
end
|
26
22
|
end
|
27
23
|
end
|
@@ -8,10 +8,29 @@ module Dry
|
|
8
8
|
|
9
9
|
DOT = '.'
|
10
10
|
|
11
|
+
# Root path is used for base errors in hash representation of error messages
|
12
|
+
ROOT_PATH = [nil].freeze
|
13
|
+
|
14
|
+
# Mapping for block kwarg options used by block_options
|
15
|
+
#
|
16
|
+
# @see Rule#block_options
|
17
|
+
BLOCK_OPTIONS_MAPPINGS = Hash.new { |_, key| key }.update(context: :_context).freeze
|
18
|
+
|
19
|
+
# Error raised when `rule` specifies one or more keys that the schema doesn't specify
|
20
|
+
InvalidKeysError = Class.new(StandardError)
|
21
|
+
|
11
22
|
# Error raised when a localized message was not found
|
12
23
|
MissingMessageError = Class.new(StandardError)
|
13
24
|
|
14
25
|
# Error raised when trying to define a schema in a contract class that already has a schema
|
15
26
|
DuplicateSchemaError = Class.new(StandardError)
|
27
|
+
|
28
|
+
# Error raised during initialization of a contract that has no schema defined
|
29
|
+
SchemaMissingError = Class.new(StandardError) do
|
30
|
+
# @api private
|
31
|
+
def initialize(klass)
|
32
|
+
super("#{klass} cannot be instantiated without a schema defined")
|
33
|
+
end
|
34
|
+
end
|
16
35
|
end
|
17
36
|
end
|
@@ -62,11 +62,6 @@ module Dry
|
|
62
62
|
# @api public
|
63
63
|
option :config, default: -> { self.class.config }
|
64
64
|
|
65
|
-
# @!attribute [r] locale
|
66
|
-
# @return [Symbol] Contract's locale (default is `:en`)
|
67
|
-
# @api public
|
68
|
-
option :locale, default: -> { self.class.config.locale }
|
69
|
-
|
70
65
|
# @!attribute [r] macros
|
71
66
|
# @return [Macros::Container] Configured macros
|
72
67
|
# @see Macros::Container#register
|
@@ -76,7 +71,7 @@ module Dry
|
|
76
71
|
# @!attribute [r] schema
|
77
72
|
# @return [Dry::Schema::Params, Dry::Schema::JSON, Dry::Schema::Processor]
|
78
73
|
# @api private
|
79
|
-
option :schema, default: -> { self.class.__schema__ }
|
74
|
+
option :schema, default: -> { self.class.__schema__ || raise(SchemaMissingError, self.class) }
|
80
75
|
|
81
76
|
# @!attribute [r] rules
|
82
77
|
# @return [Hash]
|
@@ -86,7 +81,7 @@ module Dry
|
|
86
81
|
# @!attribute [r] message_resolver
|
87
82
|
# @return [Messages::Resolver]
|
88
83
|
# @api private
|
89
|
-
option :message_resolver, default: -> { Messages::Resolver.new(
|
84
|
+
option :message_resolver, default: -> { Messages::Resolver.new(messages) }
|
90
85
|
|
91
86
|
# Apply the contract to an input
|
92
87
|
#
|
@@ -96,11 +91,13 @@ module Dry
|
|
96
91
|
#
|
97
92
|
# @api public
|
98
93
|
def call(input)
|
99
|
-
Result.new(schema.(input), Concurrent::Map.new
|
94
|
+
Result.new(schema.(input), Concurrent::Map.new) do |result|
|
100
95
|
rules.each do |rule|
|
101
96
|
next if rule.keys.any? { |key| error?(result, key) }
|
102
97
|
|
103
|
-
rule.(self, result)
|
98
|
+
rule_result = rule.(self, result)
|
99
|
+
|
100
|
+
rule_result.failures.each do |failure|
|
104
101
|
result.add_error(message_resolver[failure])
|
105
102
|
end
|
106
103
|
end
|
@@ -121,7 +118,7 @@ module Dry
|
|
121
118
|
# @api private
|
122
119
|
def error?(result, key)
|
123
120
|
path = Schema::Path[key]
|
124
|
-
result.error?(path) || path.map.with_index { |
|
121
|
+
result.error?(path) || path.map.with_index { |_k, i| result.error?(path.keys[0..i - 2]) }.any?
|
125
122
|
end
|
126
123
|
|
127
124
|
# Get a registered macro
|
@@ -129,8 +126,17 @@ module Dry
|
|
129
126
|
# @return [Proc,#to_proc]
|
130
127
|
#
|
131
128
|
# @api private
|
132
|
-
def macro(name)
|
133
|
-
macros.key?(name) ? macros[name] : Macros[name]
|
129
|
+
def macro(name, *args)
|
130
|
+
(macros.key?(name) ? macros[name] : Macros[name]).with(args)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Return configured messages backend
|
134
|
+
#
|
135
|
+
# @return [Dry::Schema::Messages::YAML, Dry::Schema::Messages::I18n]
|
136
|
+
#
|
137
|
+
# @api private
|
138
|
+
def messages
|
139
|
+
self.class.messages
|
134
140
|
end
|
135
141
|
end
|
136
142
|
end
|
@@ -2,10 +2,39 @@
|
|
2
2
|
|
3
3
|
require 'dry/schema'
|
4
4
|
require 'dry/schema/messages'
|
5
|
+
require 'dry/schema/path'
|
6
|
+
require 'dry/schema/key_map'
|
5
7
|
|
6
8
|
require 'dry/validation/constants'
|
9
|
+
require 'dry/validation/macros'
|
7
10
|
|
8
11
|
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
|
+
|
9
38
|
module Validation
|
10
39
|
class Contract
|
11
40
|
# Contract's class interface
|
@@ -14,6 +43,8 @@ module Dry
|
|
14
43
|
#
|
15
44
|
# @api public
|
16
45
|
module ClassInterface
|
46
|
+
include Macros::Registrar
|
47
|
+
|
17
48
|
# @api private
|
18
49
|
def inherited(klass)
|
19
50
|
super
|
@@ -43,27 +74,6 @@ module Dry
|
|
43
74
|
config.macros
|
44
75
|
end
|
45
76
|
|
46
|
-
# Register a new global macro
|
47
|
-
#
|
48
|
-
# Macros will be available for the contract class and its descendants
|
49
|
-
#
|
50
|
-
# @example
|
51
|
-
# class MyContract < Dry::Validation::Contract
|
52
|
-
# register_macro(:even_numbers) do
|
53
|
-
# key.failure('all numbers must be even') unless values[key_name].all?(&:even?)
|
54
|
-
# end
|
55
|
-
# end
|
56
|
-
#
|
57
|
-
# @param [Symbol] name The name of the macro
|
58
|
-
#
|
59
|
-
# @return [self]
|
60
|
-
#
|
61
|
-
# @api public
|
62
|
-
def register_macro(name, &block)
|
63
|
-
macros.register(name, &block)
|
64
|
-
self
|
65
|
-
end
|
66
|
-
|
67
77
|
# Define a params schema for your contract
|
68
78
|
#
|
69
79
|
# This type of schema is suitable for HTTP parameters
|
@@ -116,6 +126,8 @@ module Dry
|
|
116
126
|
#
|
117
127
|
# @api public
|
118
128
|
def rule(*keys, &block)
|
129
|
+
ensure_valid_keys(*keys)
|
130
|
+
|
119
131
|
Rule.new(keys: keys, block: block).tap do |rule|
|
120
132
|
rules << rule
|
121
133
|
end
|
@@ -166,6 +178,26 @@ module Dry
|
|
166
178
|
|
167
179
|
private
|
168
180
|
|
181
|
+
# @api private
|
182
|
+
def ensure_valid_keys(*keys)
|
183
|
+
valid_paths = key_map.to_dot_notation.map { |value| Schema::Path[value] }
|
184
|
+
|
185
|
+
invalid_keys = Schema::KeyMap[*keys]
|
186
|
+
.map(&:dump)
|
187
|
+
.reject { |spec| valid_paths.any? { |path| path.include?(Schema::Path[spec]) } }
|
188
|
+
|
189
|
+
return if invalid_keys.empty?
|
190
|
+
|
191
|
+
raise InvalidKeysError, <<~STR.strip
|
192
|
+
#{name}.rule specifies keys that are not defined by the schema: #{invalid_keys.inspect}
|
193
|
+
STR
|
194
|
+
end
|
195
|
+
|
196
|
+
# @api private
|
197
|
+
def key_map
|
198
|
+
__schema__.key_map
|
199
|
+
end
|
200
|
+
|
169
201
|
# @api private
|
170
202
|
def schema_opts
|
171
203
|
{ parent: superclass&.__schema__, config: config }
|
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'dry/initializer'
|
4
|
+
|
4
5
|
require 'dry/validation/constants'
|
6
|
+
require 'dry/validation/failures'
|
5
7
|
|
6
8
|
module Dry
|
7
9
|
module Validation
|
@@ -15,50 +17,16 @@ module Dry
|
|
15
17
|
class Evaluator
|
16
18
|
extend Dry::Initializer
|
17
19
|
|
18
|
-
ROOT_PATH = [nil].freeze
|
19
|
-
|
20
|
-
# Failure accumulator object
|
21
|
-
#
|
22
|
-
# @api public
|
23
|
-
class Failures
|
24
|
-
# @api private
|
25
|
-
attr_reader :path
|
26
|
-
|
27
|
-
# @api private
|
28
|
-
attr_reader :opts
|
29
|
-
|
30
|
-
# @api private
|
31
|
-
def initialize(path = ROOT_PATH)
|
32
|
-
@path = Dry::Schema::Path[path]
|
33
|
-
@opts = []
|
34
|
-
end
|
35
|
-
|
36
|
-
# Set failure
|
37
|
-
#
|
38
|
-
# @overload failure(message)
|
39
|
-
# Set message text explicitly
|
40
|
-
# @param message [String] The message text
|
41
|
-
# @example
|
42
|
-
# failure('this failed')
|
43
|
-
#
|
44
|
-
# @overload failure(id)
|
45
|
-
# Use message identifier (needs localized messages setup)
|
46
|
-
# @param id [Symbol] The message id
|
47
|
-
# @example
|
48
|
-
# failure(:taken)
|
49
|
-
#
|
50
|
-
# @api public
|
51
|
-
def failure(message, tokens = EMPTY_HASH)
|
52
|
-
@opts << { message: message, tokens: tokens, path: path }
|
53
|
-
self
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
20
|
# @!attribute [r] _contract
|
58
21
|
# @return [Contract]
|
59
22
|
# @api private
|
60
23
|
param :_contract
|
61
24
|
|
25
|
+
# @!attribute [r] result
|
26
|
+
# @return [Result]
|
27
|
+
# @api private
|
28
|
+
option :result
|
29
|
+
|
62
30
|
# @!attribute [r] keys
|
63
31
|
# @return [Array<String, Symbol, Hash>]
|
64
32
|
# @api private
|
@@ -84,16 +52,30 @@ module Dry
|
|
84
52
|
# @api private
|
85
53
|
option :values
|
86
54
|
|
55
|
+
# @!attribute [r] block_options
|
56
|
+
# @return [Hash<Symbol=>Symbol>]
|
57
|
+
# @api private
|
58
|
+
option :block_options, default: proc { EMPTY_HASH }
|
59
|
+
|
60
|
+
# @return [Hash]
|
61
|
+
attr_reader :_options
|
62
|
+
|
87
63
|
# Initialize a new evaluator
|
88
64
|
#
|
89
65
|
# @api private
|
90
|
-
def initialize(
|
91
|
-
super(
|
66
|
+
def initialize(contract, options, &block)
|
67
|
+
super(contract, options)
|
92
68
|
|
93
|
-
|
69
|
+
@_options = options
|
94
70
|
|
95
|
-
|
96
|
-
|
71
|
+
if block
|
72
|
+
exec_opts = block_options.map { |key, value| [key, _options[value]] }.to_h
|
73
|
+
instance_exec(exec_opts, &block)
|
74
|
+
end
|
75
|
+
|
76
|
+
macros.each do |args|
|
77
|
+
macro = macro(*args.flatten(1))
|
78
|
+
instance_exec(macro.extract_block_options(_options.merge(macro: macro)), ¯o.block)
|
97
79
|
end
|
98
80
|
end
|
99
81
|
|
@@ -127,10 +109,15 @@ module Dry
|
|
127
109
|
#
|
128
110
|
# @api private
|
129
111
|
def failures
|
130
|
-
failures
|
131
|
-
failures += @base.opts if defined?(@base)
|
132
|
-
failures.concat(@key.values.flat_map(&:opts)) if defined?(@key)
|
133
|
-
failures
|
112
|
+
@failures ||= []
|
113
|
+
@failures += @base.opts if defined?(@base)
|
114
|
+
@failures.concat(@key.values.flat_map(&:opts)) if defined?(@key)
|
115
|
+
@failures
|
116
|
+
end
|
117
|
+
|
118
|
+
# @api private
|
119
|
+
def with(new_opts, &block)
|
120
|
+
self.class.new(_contract, _options.merge(new_opts), &block)
|
134
121
|
end
|
135
122
|
|
136
123
|
# Return default (first) key name
|
@@ -142,6 +129,35 @@ module Dry
|
|
142
129
|
@key_name ||= keys.first
|
143
130
|
end
|
144
131
|
|
132
|
+
# Return the value found under the first specified key
|
133
|
+
#
|
134
|
+
# This is a convenient method that can be used in all the common cases
|
135
|
+
# where a rule depends on just one key and you want a quick access to
|
136
|
+
# the value
|
137
|
+
#
|
138
|
+
# @example
|
139
|
+
# rule(:age) do
|
140
|
+
# key.failure(:invalid) if value < 18
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# @return [Object]
|
144
|
+
#
|
145
|
+
# @public
|
146
|
+
def value
|
147
|
+
values[key_name]
|
148
|
+
end
|
149
|
+
|
150
|
+
# Check if there are any errors under the provided path
|
151
|
+
#
|
152
|
+
# @param [Symbol, String, Array] A Path-compatible spec
|
153
|
+
#
|
154
|
+
# @return [Boolean]
|
155
|
+
#
|
156
|
+
# @api public
|
157
|
+
def error?(path)
|
158
|
+
result.error?(path)
|
159
|
+
end
|
160
|
+
|
145
161
|
# @api private
|
146
162
|
def respond_to_missing?(meth, include_private = false)
|
147
163
|
super || _contract.respond_to?(meth, true)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/schema/path'
|
4
|
+
require 'dry/validation/constants'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Validation
|
8
|
+
# Failure accumulator object
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Failures
|
12
|
+
# The path for messages accumulated by failures object
|
13
|
+
#
|
14
|
+
# @return [Dry::Schema::Path]
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
attr_reader :path
|
18
|
+
|
19
|
+
# Options for messages
|
20
|
+
#
|
21
|
+
# These options are used by MessageResolver
|
22
|
+
#
|
23
|
+
# @return [Hash]
|
24
|
+
#
|
25
|
+
# @api private
|
26
|
+
attr_reader :opts
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
def initialize(path = ROOT_PATH)
|
30
|
+
@path = Dry::Schema::Path[path]
|
31
|
+
@opts = EMPTY_ARRAY.dup
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set failure
|
35
|
+
#
|
36
|
+
# @overload failure(message)
|
37
|
+
# Set message text explicitly
|
38
|
+
# @param message [String] The message text
|
39
|
+
# @example
|
40
|
+
# failure('this failed')
|
41
|
+
#
|
42
|
+
# @overload failure(id)
|
43
|
+
# Use message identifier (needs localized messages setup)
|
44
|
+
# @param id [Symbol] The message id
|
45
|
+
# @example
|
46
|
+
# failure(:taken)
|
47
|
+
#
|
48
|
+
# @see Evaluator#key
|
49
|
+
# @see Evaluator#base
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def failure(message, tokens = EMPTY_HASH)
|
53
|
+
opts << { message: message, tokens: tokens, path: path }
|
54
|
+
self
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/initializer'
|
4
|
+
require 'dry/validation/constants'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Validation
|
8
|
+
# Abstract class for handling rule blocks
|
9
|
+
#
|
10
|
+
# @see Rule
|
11
|
+
# @see Macro
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
class Function
|
15
|
+
extend Dry::Initializer
|
16
|
+
|
17
|
+
# @!attribute [r] block
|
18
|
+
# @return [Proc]
|
19
|
+
# @api private
|
20
|
+
option :block
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Extract options for the block kwargs
|
25
|
+
#
|
26
|
+
# @return [Hash]
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
def block_options
|
30
|
+
return EMPTY_HASH unless block
|
31
|
+
|
32
|
+
@block_options ||= block
|
33
|
+
.parameters
|
34
|
+
.select { |arg| arg[0].equal?(:keyreq) }
|
35
|
+
.map(&:last)
|
36
|
+
.map { |name| [name, BLOCK_OPTIONS_MAPPINGS[name]] }
|
37
|
+
.to_h
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/validation/constants'
|
4
|
+
require 'dry/validation/function'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Validation
|
8
|
+
# A wrapper for macro validation blocks
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Macro < Function
|
12
|
+
# @!attribute [r] name
|
13
|
+
# @return [Symbol]
|
14
|
+
# @api public
|
15
|
+
param :name
|
16
|
+
|
17
|
+
# @!attribute [r] args
|
18
|
+
# @return [Array]
|
19
|
+
# @api public
|
20
|
+
option :args
|
21
|
+
|
22
|
+
# @!attribute [r] block
|
23
|
+
# @return [Proc]
|
24
|
+
# @api private
|
25
|
+
option :block
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
def with(args)
|
29
|
+
self.class.new(name, args: args, block: block)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
def extract_block_options(options)
|
34
|
+
block_options.map { |key, value| [key, options[value]] }.to_h
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'dry/container'
|
4
|
+
require 'dry/validation/macro'
|
4
5
|
|
5
6
|
module Dry
|
6
7
|
module Validation
|
@@ -8,6 +9,35 @@ module Dry
|
|
8
9
|
#
|
9
10
|
# @api public
|
10
11
|
module Macros
|
12
|
+
module Registrar
|
13
|
+
# Register a macro
|
14
|
+
#
|
15
|
+
# @example register a global macro
|
16
|
+
# Dry::Validation.register_macro(:even_numbers) do
|
17
|
+
# key.failure('all numbers must be even') unless values[key_name].all?(&:even?)
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# @example register a contract macro
|
21
|
+
# class MyContract < Dry::Validation::Contract
|
22
|
+
# register_macro(:even_numbers) do
|
23
|
+
# key.failure('all numbers must be even') unless values[key_name].all?(&:even?)
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# @param [Symbol] name The name of the macro
|
28
|
+
# @param [Array] *args Optional default arguments for the macro
|
29
|
+
#
|
30
|
+
# @return [self]
|
31
|
+
#
|
32
|
+
# @see Macro
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
def register_macro(name, *args, &block)
|
36
|
+
macros.register(name, *args, &block)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
11
41
|
# Registry for macros
|
12
42
|
#
|
13
43
|
# @api public
|
@@ -28,8 +58,9 @@ module Dry
|
|
28
58
|
# @return [self]
|
29
59
|
#
|
30
60
|
# @api public
|
31
|
-
def register(name, &block)
|
32
|
-
|
61
|
+
def register(name, *args, &block)
|
62
|
+
macro = Macro.new(name, args: args, block: block)
|
63
|
+
super(name, macro, call: false, &nil)
|
33
64
|
self
|
34
65
|
end
|
35
66
|
end
|
@@ -52,8 +83,8 @@ module Dry
|
|
52
83
|
# @return [Macros]
|
53
84
|
#
|
54
85
|
# @api public
|
55
|
-
def self.register(*args, &block)
|
56
|
-
container.register(*args, &block)
|
86
|
+
def self.register(name, *args, &block)
|
87
|
+
container.register(name, *args, &block)
|
57
88
|
self
|
58
89
|
end
|
59
90
|
|
@@ -27,7 +27,7 @@ module Dry
|
|
27
27
|
|
28
28
|
# @api private
|
29
29
|
def initialize(messages, options = EMPTY_HASH)
|
30
|
-
@locale = options
|
30
|
+
@locale = options[:locale]
|
31
31
|
@source_messages = options.fetch(:source) { messages.dup }
|
32
32
|
super
|
33
33
|
end
|
@@ -38,7 +38,7 @@ module Dry
|
|
38
38
|
#
|
39
39
|
# @api private
|
40
40
|
def with(other, new_options = EMPTY_HASH)
|
41
|
-
return self if new_options.empty?
|
41
|
+
return self if new_options.empty? && other.eql?(messages)
|
42
42
|
|
43
43
|
self.class.new(
|
44
44
|
(other + select { |err| err.is_a?(Message) }).uniq,
|
@@ -14,15 +14,9 @@ module Dry
|
|
14
14
|
# @api private
|
15
15
|
attr_reader :messages
|
16
16
|
|
17
|
-
# @!attribute [r] locale
|
18
|
-
# @return [Symbol] current locale
|
19
|
-
# @api private
|
20
|
-
attr_reader :locale
|
21
|
-
|
22
17
|
# @api private
|
23
|
-
def initialize(messages
|
18
|
+
def initialize(messages)
|
24
19
|
@messages = messages
|
25
|
-
@locale = locale
|
26
20
|
end
|
27
21
|
|
28
22
|
# Resolve Message object from provided args and path
|
@@ -55,9 +49,9 @@ module Dry
|
|
55
49
|
# @return [String]
|
56
50
|
#
|
57
51
|
# @api public
|
58
|
-
def message(rule, tokens: EMPTY_HASH,
|
52
|
+
def message(rule, tokens: EMPTY_HASH, locale: nil, full: false, path:)
|
59
53
|
keys = path.to_a.compact
|
60
|
-
msg_opts = tokens.merge(path: keys, locale: locale)
|
54
|
+
msg_opts = tokens.merge(path: keys, locale: locale || messages.default_locale)
|
61
55
|
|
62
56
|
if keys.empty?
|
63
57
|
template, meta = messages["rules.#{rule}", msg_opts]
|
data/lib/dry/validation/rule.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'dry/equalizer'
|
4
|
-
require 'dry/initializer'
|
5
4
|
|
6
5
|
require 'dry/validation/constants'
|
6
|
+
require 'dry/validation/function'
|
7
7
|
|
8
8
|
module Dry
|
9
9
|
module Validation
|
@@ -15,11 +15,9 @@ module Dry
|
|
15
15
|
# @see Contract#rule
|
16
16
|
#
|
17
17
|
# @api public
|
18
|
-
class Rule
|
18
|
+
class Rule < Function
|
19
19
|
include Dry::Equalizer(:keys, :block, inspect: false)
|
20
20
|
|
21
|
-
extend Dry::Initializer
|
22
|
-
|
23
21
|
# @!attribute [r] keys
|
24
22
|
# @return [Array<Symbol, String, Hash>]
|
25
23
|
# @api private
|
@@ -30,11 +28,6 @@ module Dry
|
|
30
28
|
# @api private
|
31
29
|
option :macros, default: proc { EMPTY_ARRAY.dup }
|
32
30
|
|
33
|
-
# @!attribute [r] block
|
34
|
-
# @return [Proc]
|
35
|
-
# @api private
|
36
|
-
option :block
|
37
|
-
|
38
31
|
# Evaluate the rule within the provided context
|
39
32
|
#
|
40
33
|
# @param [Contract] contract
|
@@ -44,7 +37,12 @@ module Dry
|
|
44
37
|
def call(contract, result)
|
45
38
|
Evaluator.new(
|
46
39
|
contract,
|
47
|
-
|
40
|
+
keys: keys,
|
41
|
+
macros: macros,
|
42
|
+
block_options: block_options,
|
43
|
+
result: result,
|
44
|
+
values: result.values,
|
45
|
+
_context: result.context,
|
48
46
|
&block
|
49
47
|
)
|
50
48
|
end
|
@@ -56,11 +54,43 @@ module Dry
|
|
56
54
|
#
|
57
55
|
# @api public
|
58
56
|
def validate(*macros, &block)
|
59
|
-
@macros = macros
|
57
|
+
@macros = macros.map { |spec| Array(spec) }.map(&:flatten)
|
60
58
|
@block = block if block
|
61
59
|
self
|
62
60
|
end
|
63
61
|
|
62
|
+
# Define a validation function for each element of an array
|
63
|
+
#
|
64
|
+
# The function will be applied only if schema checks passed
|
65
|
+
# for a given array item.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# rule(:nums).each do
|
69
|
+
# key.failure("must be greater than 0") if value < 0
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# @return [Rule]
|
73
|
+
#
|
74
|
+
# @api public
|
75
|
+
def each(*macros, &block)
|
76
|
+
root = keys
|
77
|
+
@keys = []
|
78
|
+
|
79
|
+
@block = proc do
|
80
|
+
values[root].each_with_index do |_, idx|
|
81
|
+
path = [*root, idx]
|
82
|
+
|
83
|
+
next if result.error?(path)
|
84
|
+
|
85
|
+
evaluator = with(macros: macros, keys: [path], &block)
|
86
|
+
|
87
|
+
failures.concat(evaluator.failures)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
64
94
|
# Return a nice string representation
|
65
95
|
#
|
66
96
|
# @return [String]
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'dry/equalizer'
|
4
|
+
require 'dry/validation/constants'
|
4
5
|
|
5
6
|
module Dry
|
6
7
|
module Validation
|
@@ -38,8 +39,18 @@ module Dry
|
|
38
39
|
# @return [Object]
|
39
40
|
#
|
40
41
|
# @api public
|
41
|
-
def [](
|
42
|
-
|
42
|
+
def [](*args)
|
43
|
+
if args.size.equal?(1)
|
44
|
+
case (key = args[0])
|
45
|
+
when Symbol then data[key]
|
46
|
+
when String then self[*key.split(DOT).map(&:to_sym)]
|
47
|
+
when Array then self[*key]
|
48
|
+
else
|
49
|
+
raise ArgumentError, '+key+ must be a symbol, string, array, or a list of keys for dig'
|
50
|
+
end
|
51
|
+
else
|
52
|
+
data.dig(*args)
|
53
|
+
end
|
43
54
|
end
|
44
55
|
|
45
56
|
# @api private
|
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.0.0
|
4
|
+
version: 1.0.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-
|
11
|
+
date: 2019-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -52,6 +52,26 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: dry-container
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.7'
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 0.7.1
|
65
|
+
type: :runtime
|
66
|
+
prerelease: false
|
67
|
+
version_requirements: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - "~>"
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0.7'
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 0.7.1
|
55
75
|
- !ruby/object:Gem::Dependency
|
56
76
|
name: dry-initializer
|
57
77
|
requirement: !ruby/object:Gem::Requirement
|
@@ -73,6 +93,9 @@ dependencies:
|
|
73
93
|
- - "~>"
|
74
94
|
- !ruby/object:Gem::Version
|
75
95
|
version: '1.0'
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 1.1.0
|
76
99
|
type: :runtime
|
77
100
|
prerelease: false
|
78
101
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -80,6 +103,9 @@ dependencies:
|
|
80
103
|
- - "~>"
|
81
104
|
- !ruby/object:Gem::Version
|
82
105
|
version: '1.0'
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: 1.1.0
|
83
109
|
- !ruby/object:Gem::Dependency
|
84
110
|
name: bundler
|
85
111
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,6 +168,9 @@ files:
|
|
142
168
|
- lib/dry/validation/evaluator.rb
|
143
169
|
- lib/dry/validation/extensions/hints.rb
|
144
170
|
- lib/dry/validation/extensions/monads.rb
|
171
|
+
- lib/dry/validation/failures.rb
|
172
|
+
- lib/dry/validation/function.rb
|
173
|
+
- lib/dry/validation/macro.rb
|
145
174
|
- lib/dry/validation/macros.rb
|
146
175
|
- lib/dry/validation/message.rb
|
147
176
|
- lib/dry/validation/message_set.rb
|
@@ -165,9 +194,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
165
194
|
version: '2.3'
|
166
195
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
167
196
|
requirements:
|
168
|
-
- - "
|
197
|
+
- - ">="
|
169
198
|
- !ruby/object:Gem::Version
|
170
|
-
version:
|
199
|
+
version: '0'
|
171
200
|
requirements: []
|
172
201
|
rubygems_version: 3.0.3
|
173
202
|
signing_key:
|