dry-validation 1.0.0.rc3 → 1.1.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: a644e1a1909528377d2edf6acb339bb6e571a9c2959700f9537ed849c9b6c0bc
4
- data.tar.gz: 22bce26482e71124a4c83c4c9d1a6dd2da2960d02b30080ff672540322247d42
3
+ metadata.gz: 4985e6191e9e249b510fceea630ed463f11c5ff07830e6f3f867e913b29860ac
4
+ data.tar.gz: 99cb37716fb12f4a09bb94a4048935e6ac8db658df4d5a4318be321c5aef90ff
5
5
  SHA512:
6
- metadata.gz: 37ddca21e417d6f4d8a6f16c10aecfb8a26f50483115d286f77ce84420ada5b67f0369d7a877d9ca7bc8d717f2ff2f8b770ffdfe66e370e570a4ab9abc7e6f38
7
- data.tar.gz: 77bcd57cbc2918991363c3cdda5302528c10370a50c1d684462e15967812643008da5129168600f41af592c31831c712c69558997c2f470bc2f8b62ca0a3c806
6
+ metadata.gz: 2d25a5df6a56b6a8ed32f19ed3991297f2d878d24ba5349fd8c8d0ec50a3503842c8b591d7b176ddf9303b78a847002e638faeb29691329db3b2f41d2379116b
7
+ data.tar.gz: a21649e2476c688f9a4275e43819640823c6e6a2caf18e4b8d03c0df97c946d5edbb517c6482918bf3b3b9bc21079e667c06986c2adc81acdeb0561b1bc264aa
@@ -1,3 +1,56 @@
1
+ # v1.1.0 2019-06-14
2
+
3
+ ### Added
4
+
5
+ * `key?` method available within rules, that can be used to check if there's a value under the rule's default key (refs #540) (@solnic)
6
+ * `value` supports hash-based path specifications now (refs #547) (@solnic)
7
+ * `value` can read multiple values when the key points to them, ie in case of `rule(geo: [:lat, :lon])` it would return an array with `lat` and `lon` (@solnic)
8
+
9
+ ### Fixed
10
+
11
+ * Passing multiple macro names to `validate` or `each` works correctly (fixed #538 #541) (@jandudulski)
12
+
13
+ [Compare v1.0.0...v1.1.0](https://github.com/dry-rb/dry-validation/compare/v1.0.0...v1.1.0)
14
+
15
+ # v1.0.0 2019-06-10
16
+
17
+ 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.
18
+
19
+ 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).
20
+
21
+ ### :sparkles: Release highlights :sparkles:
22
+
23
+ - New `Dry::Validation::Contract` API for defining contract classes with schemas and validation rules
24
+ - Improved message backends with support for `key` and `base` messages, and arbitrary meta-data (like error codes etc.)
25
+ - Support for defining rules for array elements ie `rule(:items).each { ... }`
26
+ - Support for macros that encapsulate common rule logic
27
+ - Built-in `:acceptance` macro
28
+
29
+ [Compare v0.13.3...v1.0.0](https://github.com/dry-rb/dry-validation/compare/v0.13.3...v1.0.0)
30
+
31
+ # v1.0.0 2019-06-10 (compared to 1.0.0.rc3)
32
+
33
+ ### Added
34
+
35
+ - Support for defining rules for each element of an array via `rule(:items).each { ... }` (solnic)
36
+ - Support for parameterized macros via `rule(:foo).validate(my_macro: :some_option)` (solnic)
37
+ - `values#[]` is now compatible with path specs (symbol, array with keys or dot-notation) (issue #528) (solnic)
38
+ - `value` shortcut for accessing the value found under the first key specified by a rule. ie `rule(:foo) { value }` returns `values[:foo]` (solnic)
39
+
40
+ ### Fixed
41
+
42
+ - 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)
43
+ - Macros no longer mutate `Dry::Validation::Contract.macros` when using inheritance (solnic)
44
+ - Missing dependency on `dry-container` was added (solnic)
45
+
46
+ ### Changed
47
+
48
+ - `rule` will raise `InvalidKeysError` when specified keys are not defined by the schema (solnic)
49
+ - `Contract.new` will raise `SchemaMissingError` when the class doesn't have schema defined (solnic)
50
+ - Contracts no longer support `:locale` option in the constructor. Use `Result#errors(locale: :pl)` to change locale at run-time (solnic)
51
+
52
+ [Compare v1.0.0.rc3...v1.0.0](https://github.com/dry-rb/dry-validation/compare/v1.0.0.rc3...v1.0.0)
53
+
1
54
  # v1.0.0.rc3 2019-05-06
2
55
 
3
56
  ### Added
@@ -15,6 +68,8 @@
15
68
 
16
69
  # v1.0.0.rc2 2019-05-04
17
70
 
71
+ 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`.
72
+
18
73
  ### Added
19
74
 
20
75
  * [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)
68
+
69
+ @_options = options
92
70
 
93
- instance_exec(_context, &block) if block
71
+ if block
72
+ exec_opts = block_options.map { |key, value| [key, _options[value]] }.to_h
73
+ instance_exec(exec_opts, &block)
74
+ end
94
75
 
95
- macros.each do |macro|
96
- instance_exec(_context, &macro(macro))
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,51 @@ 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
+ # Return if the value under the default key is available
151
+ #
152
+ # This is useful when dealing with rules for optional keys
153
+ #
154
+ # @example
155
+ # rule(:age) do
156
+ # key.failure(:invalid) if key? && value < 18
157
+ # end
158
+ #
159
+ # @return [Boolean]
160
+ #
161
+ # @api public
162
+ def key?
163
+ values.key?(key_name)
164
+ end
165
+
166
+ # Check if there are any errors under the provided path
167
+ #
168
+ # @param [Symbol, String, Array] A Path-compatible spec
169
+ #
170
+ # @return [Boolean]
171
+ #
172
+ # @api public
173
+ def error?(path)
174
+ result.error?(path)
175
+ end
176
+
145
177
  # @api private
146
178
  def respond_to_missing?(meth, include_private = false)
147
179
  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,45 @@ module Dry
56
54
  #
57
55
  # @api public
58
56
  def validate(*macros, &block)
59
- @macros = macros
57
+ @macros = parse_macros(*macros)
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
+ # rule(:nums).each(min: 3)
72
+ #
73
+ # @return [Rule]
74
+ #
75
+ # @api public
76
+ def each(*macros, &block)
77
+ root = keys
78
+ macros = parse_macros(*macros)
79
+ @keys = []
80
+
81
+ @block = proc do
82
+ values[root].each_with_index do |_, idx|
83
+ path = [*root, idx]
84
+
85
+ next if result.error?(path)
86
+
87
+ evaluator = with(macros: macros, keys: [path], &block)
88
+
89
+ failures.concat(evaluator.failures)
90
+ end
91
+ end
92
+
93
+ self
94
+ end
95
+
64
96
  # Return a nice string representation
65
97
  #
66
98
  # @return [String]
@@ -69,6 +101,22 @@ module Dry
69
101
  def inspect
70
102
  %(#<#{self.class} keys=#{keys.inspect}>)
71
103
  end
104
+
105
+ # Parse function arguments into macros structure
106
+ #
107
+ # @return [Array]
108
+ #
109
+ # @api private
110
+ def parse_macros(*args)
111
+ args.each_with_object([]) do |spec, macros|
112
+ case spec
113
+ when Hash
114
+ spec.each { |k, v| macros << [k, Array(v)] }
115
+ else
116
+ macros << Array(spec)
117
+ end
118
+ end
119
+ end
72
120
  end
73
121
  end
74
122
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dry/equalizer'
4
+ require 'dry/schema/path'
5
+ require 'dry/validation/constants'
4
6
 
5
7
  module Dry
6
8
  module Validation
@@ -38,8 +40,40 @@ module Dry
38
40
  # @return [Object]
39
41
  #
40
42
  # @api public
41
- def [](key)
42
- data[key]
43
+ def [](*args)
44
+ return data.dig(*args) if args.size > 1
45
+
46
+ case (key = args[0])
47
+ when Symbol, String, Array, Hash
48
+ path = Schema::Path[key]
49
+ keys = path.to_a
50
+
51
+ return data.dig(*keys) unless keys.last.is_a?(Array)
52
+
53
+ last = keys.pop
54
+ vals = self.class.new(data.dig(*keys))
55
+
56
+ last.map { |name| vals[name] }
57
+ else
58
+ raise ArgumentError, '+key+ must be a valid path specification'
59
+ end
60
+ end
61
+
62
+ # @api public
63
+ def key?(key, hash = data)
64
+ return hash.key?(key) if key.is_a?(Symbol)
65
+
66
+ Schema::Path[key].reduce(hash) do |a, e|
67
+ if e.is_a?(Array)
68
+ result = e.all? { |k| key?(k, a) }
69
+ return result
70
+ else
71
+ return false unless a.is_a?(Array) ? (e >= 0 && e < a.size) : a.key?(e)
72
+ end
73
+ a[e]
74
+ end
75
+
76
+ true
43
77
  end
44
78
 
45
79
  # @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.1.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.1.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-14 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: