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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a644e1a1909528377d2edf6acb339bb6e571a9c2959700f9537ed849c9b6c0bc
4
- data.tar.gz: 22bce26482e71124a4c83c4c9d1a6dd2da2960d02b30080ff672540322247d42
3
+ metadata.gz: 7acb8c840fe5a4c12564c86985078e25cff1bea877b9e2dc4f29b39bc892b6a5
4
+ data.tar.gz: ae09dc37911b0e4031918f22efabd53035501119a71da60ce08b4e0a00a3e0b6
5
5
  SHA512:
6
- metadata.gz: 37ddca21e417d6f4d8a6f16c10aecfb8a26f50483115d286f77ce84420ada5b67f0369d7a877d9ca7bc8d717f2ff2f8b770ffdfe66e370e570a4ab9abc7e6f38
7
- data.tar.gz: 77bcd57cbc2918991363c3cdda5302528c10370a50c1d684462e15967812643008da5129168600f41af592c31831c712c69558997c2f470bc2f8b62ca0a3c806
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)
@@ -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 macros
19
- config.macros
20
- end
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(self.class.messages, locale) }
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, locale: locale) do |result|
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).failures.each do |failure|
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 { |k, i| result.error?(path.keys[0..i-2]) }.any?
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(*args, &block)
91
- super(*args)
66
+ def initialize(contract, options, &block)
67
+ super(contract, options)
92
68
 
93
- instance_exec(_context, &block) if block
69
+ @_options = options
94
70
 
95
- macros.each do |macro|
96
- instance_exec(_context, &macro(macro))
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)), &macro.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
- super(name, block, call: false)
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.fetch(:locale, :en)
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, locale = :en)
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, path:, locale: self.locale, full: false)
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]
@@ -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
- values: result.values, keys: keys, macros: macros, _context: result.context,
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 [](key)
42
- data[key]
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dry
4
4
  module Validation
5
- VERSION = '1.0.0.rc3'
5
+ VERSION = '1.0.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.0.0.rc3
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-05-06 00:00:00.000000000 Z
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: 1.3.1
199
+ version: '0'
171
200
  requirements: []
172
201
  rubygems_version: 3.0.3
173
202
  signing_key: