dry-validation 1.0.0.alpha2 → 1.0.0.beta1
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 +21 -0
- data/lib/dry/validation/config.rb +10 -0
- data/lib/dry/validation/contract/class_interface.rb +24 -6
- data/lib/dry/validation/contract.rb +21 -55
- data/lib/dry/validation/error.rb +66 -0
- data/lib/dry/validation/error_set.rb +139 -0
- data/lib/dry/validation/evaluator.rb +77 -52
- data/lib/dry/validation/messages/resolver.rb +72 -0
- data/lib/dry/validation/result.rb +36 -54
- data/lib/dry/validation/rule.rb +5 -4
- data/lib/dry/validation/version.rb +1 -1
- metadata +13 -30
- data/lib/dry/validation/messages.rb +0 -58
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6a47d27d49e9894cf283af7e4809cea07f45e5f8f3f52a180dd20d8c849b315
|
4
|
+
data.tar.gz: 83f1e9b5ff94c4db24769624dd1552f0876be541926ffd57d43c2490757ab662
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 67ab872380ecdc45fbe1efeb8cec95a03dff692aae2e74ca498483ceb7104b6a2d5cd78bdc23884d69d89665f110f79c3fa67285b97a40cc2c3625f9744bc34f
|
7
|
+
data.tar.gz: f306b9dc73085526c485a7b18f2e33c83d08dd72922f1640bbc9b3b6ea0164ad1fbda752da0bb9b5d1f55dbd47c6ca6aa9ebef297035fd7adb22bb18be879c4e
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
# v1.0.0.beta1 2019-03-26
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* New API for setting failures `base.failure` for base errors and `key.failure` for key errors (solnic)
|
6
|
+
* Support for `base` errors associated with a key even when child keys have errors too (solnic)
|
7
|
+
* Support for `base` errors not associated with any key (solnic)
|
8
|
+
* Result objects use `ErrorSet` object now for managing messages (solnic)
|
9
|
+
* Nested keys are properly handled when generating messages hash (issue #489) (flash-gordon + solnic)
|
10
|
+
* Result objects support `locale` and `full` options now (solnic)
|
11
|
+
* Ability to configure `top_namespace` for messages, which will be used for both schema and rule localization (solnic)
|
12
|
+
* Rule blocks receive a context object that you can use to share data between rules (solnic)
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
|
16
|
+
* [BREAKING] `Result#errors` returns an instance of `ErrorSet` now, it's an enumerable, coerible to a hash (solnic)
|
17
|
+
* [BREAKING] `failure` was removed in favor of `key.failure` or `key(:foo).failure` (solnic)
|
18
|
+
* [BREAKING] `Result#to_hash` was removed (flash-gordon)
|
19
|
+
|
20
|
+
[Compare v1.0.0.alpha2...v1.0.0.beta1](https://github.com/dry-rb/dry-validation/compare/v1.0.0.alpha2...v1.0.0.beta1)
|
21
|
+
|
1
22
|
# v1.0.0.alpha2 2019-03-05
|
2
23
|
|
3
24
|
First round of bug fixes. Thanks for testing <3!
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'dry/schema'
|
4
|
-
require 'dry/
|
4
|
+
require 'dry/schema/messages'
|
5
|
+
|
5
6
|
require 'dry/validation/constants'
|
6
7
|
|
7
8
|
module Dry
|
@@ -11,6 +12,21 @@ module Dry
|
|
11
12
|
#
|
12
13
|
# @api public
|
13
14
|
module ClassInterface
|
15
|
+
# @api private
|
16
|
+
def inherited(klass)
|
17
|
+
super
|
18
|
+
klass.instance_variable_set('@config', config.dup)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Configuration
|
22
|
+
#
|
23
|
+
# @return [Config]
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
def config
|
27
|
+
@config ||= Validation::Config.new
|
28
|
+
end
|
29
|
+
|
14
30
|
# Define a params schema for your contract
|
15
31
|
#
|
16
32
|
# This type of schema is suitable for HTTP parameters
|
@@ -78,14 +94,14 @@ module Dry
|
|
78
94
|
|
79
95
|
# @api private
|
80
96
|
def rules
|
81
|
-
@
|
82
|
-
|
83
|
-
|
97
|
+
@rules ||= EMPTY_ARRAY
|
98
|
+
.dup
|
99
|
+
.concat(superclass.respond_to?(:rules) ? superclass.rules : EMPTY_ARRAY)
|
84
100
|
end
|
85
101
|
|
86
102
|
# @api private
|
87
103
|
def messages
|
88
|
-
@
|
104
|
+
@messages ||= Schema::Messages.setup(config.messages)
|
89
105
|
end
|
90
106
|
|
91
107
|
private
|
@@ -97,7 +113,9 @@ module Dry
|
|
97
113
|
|
98
114
|
# @api private
|
99
115
|
def define(method_name, &block)
|
100
|
-
|
116
|
+
if defined?(@__schema__)
|
117
|
+
raise ::Dry::Validation::DuplicateSchemaError, 'Schema has already been defined'
|
118
|
+
end
|
101
119
|
|
102
120
|
case method_name
|
103
121
|
when :schema
|
@@ -1,13 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'concurrent/map'
|
4
|
+
|
3
5
|
require 'dry/equalizer'
|
4
|
-
require 'dry/configurable'
|
5
6
|
require 'dry/initializer'
|
6
7
|
|
8
|
+
require 'dry/validation/config'
|
7
9
|
require 'dry/validation/constants'
|
8
10
|
require 'dry/validation/rule'
|
9
11
|
require 'dry/validation/evaluator'
|
12
|
+
require 'dry/validation/messages/resolver'
|
10
13
|
require 'dry/validation/result'
|
14
|
+
require 'dry/validation/error'
|
11
15
|
require 'dry/validation/contract/class_interface'
|
12
16
|
|
13
17
|
module Dry
|
@@ -47,40 +51,20 @@ module Dry
|
|
47
51
|
class Contract
|
48
52
|
include Dry::Equalizer(:schema, :rules, :messages)
|
49
53
|
|
50
|
-
extend Dry::Configurable
|
51
54
|
extend Dry::Initializer
|
52
55
|
extend ClassInterface
|
53
56
|
|
54
|
-
|
55
|
-
|
56
|
-
# @overload config.messages=(identifier)
|
57
|
-
# Set message backend
|
58
|
-
#
|
59
|
-
# @param identifier [Symbol] the backend identifier, either `:yaml` or `:i18n`
|
60
|
-
#
|
61
|
-
# @api public
|
62
|
-
# @!scope class
|
63
|
-
setting :messages, :yaml
|
57
|
+
config.messages.top_namespace = 'dry_validation'
|
64
58
|
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
# @param path [String, Pathname] the path
|
69
|
-
#
|
59
|
+
# @!attribute [r] config
|
60
|
+
# @return [Config]
|
70
61
|
# @api public
|
71
|
-
|
72
|
-
setting :messages_file
|
62
|
+
option :config, default: -> { self.class.config }
|
73
63
|
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
77
|
-
# @param name [Symbol] the namespace
|
78
|
-
#
|
64
|
+
# @!attribute [r] locale
|
65
|
+
# @return [Symbol]
|
79
66
|
# @api public
|
80
|
-
|
81
|
-
setting :namespace
|
82
|
-
|
83
|
-
# @!endgroup
|
67
|
+
option :locale, default: -> { :en }
|
84
68
|
|
85
69
|
# @!attribute [r] schema
|
86
70
|
# @return [Dry::Schema::Params, Dry::Schema::JSON, Dry::Schema::Processor]
|
@@ -92,10 +76,10 @@ module Dry
|
|
92
76
|
# @api private
|
93
77
|
option :rules, default: -> { self.class.rules }
|
94
78
|
|
95
|
-
# @!attribute [r]
|
96
|
-
# @return [Messages::
|
79
|
+
# @!attribute [r] message_resolver
|
80
|
+
# @return [Messages::Resolver]
|
97
81
|
# @api private
|
98
|
-
option :
|
82
|
+
option :message_resolver, default: -> { Messages::Resolver.new(self.class.messages, locale) }
|
99
83
|
|
100
84
|
# Apply contract to an input
|
101
85
|
#
|
@@ -103,36 +87,18 @@ module Dry
|
|
103
87
|
#
|
104
88
|
# @api public
|
105
89
|
def call(input)
|
106
|
-
Result.new(schema.(input)) do |result|
|
90
|
+
Result.new(schema.(input), locale: locale) do |result|
|
91
|
+
context = Concurrent::Map.new
|
92
|
+
|
107
93
|
rules.each do |rule|
|
108
94
|
next if rule.keys.any? { |key| result.error?(key) }
|
109
95
|
|
110
|
-
|
111
|
-
|
96
|
+
rule.(self, result, context).failures.each do |failure|
|
97
|
+
result.add_error(message_resolver[failure])
|
98
|
+
end
|
112
99
|
end
|
113
100
|
end
|
114
101
|
end
|
115
|
-
|
116
|
-
# Get message text for the given rule name and options
|
117
|
-
#
|
118
|
-
# @return [String]
|
119
|
-
#
|
120
|
-
# @api private
|
121
|
-
def message(key, tokens: EMPTY_HASH, **opts)
|
122
|
-
msg_opts = opts.merge(tokens)
|
123
|
-
rule_path = Array(opts.fetch(:rule)).flatten
|
124
|
-
|
125
|
-
template = messages[key, msg_opts.merge(rule: rule_path.join(DOT))]
|
126
|
-
template ||= messages[key, msg_opts.merge(rule: rule_path.last)]
|
127
|
-
|
128
|
-
unless template
|
129
|
-
raise MissingMessageError, <<~STR
|
130
|
-
Message template for #{key.inspect} under #{rule_path.join(DOT).inspect} was not found
|
131
|
-
STR
|
132
|
-
end
|
133
|
-
|
134
|
-
template.(template.data(tokens))
|
135
|
-
end
|
136
102
|
end
|
137
103
|
end
|
138
104
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/equalizer'
|
4
|
+
require 'dry/schema/message'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Validation
|
8
|
+
# Error message
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Error < Schema::Message
|
12
|
+
include Dry::Equalizer(:text, :path)
|
13
|
+
|
14
|
+
# @!attribute [r] text
|
15
|
+
# @return [String] text The error message text
|
16
|
+
attr_reader :text
|
17
|
+
|
18
|
+
# @!attribute [r] path
|
19
|
+
# @return [Array<Symbol, Integer>] path The path to the value with the error
|
20
|
+
attr_reader :path
|
21
|
+
|
22
|
+
# @api public
|
23
|
+
class Localized < Error
|
24
|
+
# @api public
|
25
|
+
def evaluate(**opts)
|
26
|
+
Error.new(text.(opts), path: path)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Build an error
|
31
|
+
#
|
32
|
+
# @return [Error, Error::Localized]
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
def self.[](text, path)
|
36
|
+
text.respond_to?(:call) ? Localized.new(text, path: path) : Error.new(text, path: path)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Initialize a new error object
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
def initialize(text, path:)
|
43
|
+
@text = text
|
44
|
+
@path = Array(path)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Check if this is a base error not associated with any key
|
48
|
+
#
|
49
|
+
# @return [Boolean]
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def base?
|
53
|
+
@base ||= path.compact.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
# Dump error to a string
|
57
|
+
#
|
58
|
+
# @return [String]
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def to_s
|
62
|
+
text
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/schema/message_set'
|
4
|
+
|
5
|
+
require 'dry/validation/constants'
|
6
|
+
require 'dry/validation/error'
|
7
|
+
|
8
|
+
module Dry
|
9
|
+
module Validation
|
10
|
+
# ErrorSet is a specialized message set for handling validation errors
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
class ErrorSet < Schema::MessageSet
|
14
|
+
# @!attribute [r] source
|
15
|
+
# Return the source set of messages used to produce final evaluated messages
|
16
|
+
# @return [Array<Error, Error::Localized, Schema::Message>]
|
17
|
+
# @api private
|
18
|
+
attr_reader :source_messages
|
19
|
+
|
20
|
+
# @!attribute [r] locale
|
21
|
+
# @return [Symbol] locale
|
22
|
+
# @api public
|
23
|
+
attr_reader :locale
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
def initialize(messages, options = EMPTY_HASH)
|
27
|
+
@locale = options.fetch(:locale, :en)
|
28
|
+
@source_messages = options.fetch(:source, messages.dup)
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return a new error set using updated options
|
33
|
+
#
|
34
|
+
# @return [ErrorSet]
|
35
|
+
#
|
36
|
+
# @api private
|
37
|
+
def with(other, new_options = EMPTY_HASH)
|
38
|
+
return self if new_options.empty?
|
39
|
+
|
40
|
+
self.class.new(
|
41
|
+
other + select { |err| err.is_a?(Error) },
|
42
|
+
options.merge(source: source_messages, **new_options)
|
43
|
+
).freeze
|
44
|
+
end
|
45
|
+
|
46
|
+
# Add a new error
|
47
|
+
#
|
48
|
+
# This is used when result is being prepared
|
49
|
+
#
|
50
|
+
# @return [ErrorSet]
|
51
|
+
#
|
52
|
+
# @api private
|
53
|
+
def add(error)
|
54
|
+
source_messages << error
|
55
|
+
messages << error
|
56
|
+
initialize_placeholders!
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
# Filter error set using provided predicates
|
61
|
+
#
|
62
|
+
# This method is open to any predicate because errors can be anything that
|
63
|
+
# implements Message API, thus they can implement whatever predicates you
|
64
|
+
# may need.
|
65
|
+
#
|
66
|
+
# @example get a list of base errors
|
67
|
+
# error_set = contract.(input).error_set
|
68
|
+
# error_set.filter(:base?)
|
69
|
+
#
|
70
|
+
# @param [Array<Symbol>] *predicates
|
71
|
+
#
|
72
|
+
# @return [ErrorSet]
|
73
|
+
#
|
74
|
+
# @api public
|
75
|
+
def filter(*predicates)
|
76
|
+
errors = select { |e|
|
77
|
+
predicates.all? { |predicate| e.respond_to?(predicate) && e.public_send(predicate) }
|
78
|
+
}
|
79
|
+
self.class.new(errors)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @api private
|
83
|
+
def freeze
|
84
|
+
source_messages.select { |err| err.respond_to?(:evaluate) }.each do |err|
|
85
|
+
idx = source_messages.index(err)
|
86
|
+
msg = err.evaluate(locale: locale, full: options[:full])
|
87
|
+
messages[idx] = msg
|
88
|
+
end
|
89
|
+
to_h
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
# @api private
|
96
|
+
def unique_paths
|
97
|
+
source_messages.uniq(&:path).map(&:path)
|
98
|
+
end
|
99
|
+
|
100
|
+
# @api private
|
101
|
+
def messages_map
|
102
|
+
@messages_map ||= reduce(placeholders) { |hash, msg|
|
103
|
+
node = msg.path.reduce(hash) { |a, e| a.is_a?(Hash) ? a[e] : a.last[e] }
|
104
|
+
(node.size > 1 ? node[0] : node) << msg.to_s
|
105
|
+
hash
|
106
|
+
}
|
107
|
+
end
|
108
|
+
|
109
|
+
# @api private
|
110
|
+
#
|
111
|
+
# rubocop:disable Metrics/AbcSize
|
112
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
113
|
+
def initialize_placeholders!
|
114
|
+
@placeholders = unique_paths.each_with_object(EMPTY_HASH.dup) { |path, hash|
|
115
|
+
curr_idx = 0
|
116
|
+
last_idx = path.size - 1
|
117
|
+
node = hash
|
118
|
+
|
119
|
+
while curr_idx <= last_idx
|
120
|
+
key = path[curr_idx]
|
121
|
+
|
122
|
+
next_node =
|
123
|
+
if node.is_a?(Array) && key.is_a?(Symbol)
|
124
|
+
node_hash = (node << [] << {}).last
|
125
|
+
node_hash[key] || (node_hash[key] = curr_idx < last_idx ? {} : [])
|
126
|
+
else
|
127
|
+
node[key] || (node[key] = curr_idx < last_idx ? {} : [])
|
128
|
+
end
|
129
|
+
|
130
|
+
node = next_node
|
131
|
+
curr_idx += 1
|
132
|
+
end
|
133
|
+
}
|
134
|
+
end
|
135
|
+
# rubocop:enable Metrics/AbcSize
|
136
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -15,103 +15,128 @@ module Dry
|
|
15
15
|
class Evaluator
|
16
16
|
extend Dry::Initializer
|
17
17
|
|
18
|
-
|
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
|
+
# @!attribute [r] _contract
|
19
58
|
# @return [Contract]
|
20
59
|
# @api private
|
21
|
-
param :
|
60
|
+
param :_contract
|
22
61
|
|
23
62
|
# @!attribute [r] keys
|
24
63
|
# @return [Array<String, Symbol, Hash>]
|
25
64
|
# @api private
|
26
65
|
option :keys
|
27
66
|
|
28
|
-
# @!attribute [r]
|
29
|
-
# @return [
|
67
|
+
# @!attribute [r] _context
|
68
|
+
# @return [Concurrent::Map]
|
69
|
+
# @api public
|
70
|
+
option :_context
|
71
|
+
|
72
|
+
# @!attribute [r] path
|
73
|
+
# @return [Dry::Schema::Path]
|
30
74
|
# @api private
|
31
|
-
option :
|
75
|
+
option :path, default: proc { Dry::Schema::Path[(key = keys.first) ? key : ROOT_PATH] }
|
32
76
|
|
33
77
|
# @!attribute [r] values
|
34
78
|
# @return [Object]
|
35
79
|
# @api private
|
36
80
|
option :values
|
37
81
|
|
38
|
-
# @!attribute [r] message
|
39
|
-
# @return [String]
|
40
|
-
# @api private
|
41
|
-
attr_reader :message
|
42
|
-
|
43
82
|
# Initialize a new evaluator
|
44
83
|
#
|
45
84
|
# @api private
|
46
85
|
def initialize(*args, &block)
|
47
86
|
super(*args)
|
48
|
-
|
49
|
-
instance_eval(&block)
|
87
|
+
instance_exec(_context, &block)
|
50
88
|
end
|
51
89
|
|
52
|
-
#
|
90
|
+
# Get failures object for the default or provided path
|
53
91
|
#
|
54
|
-
# @
|
55
|
-
# Set message text explicitly
|
56
|
-
# @param message [String] The message text
|
57
|
-
# @example
|
58
|
-
# failure('this failed')
|
92
|
+
# @param [Symbol,String,Hash,Array<Symbol>] path
|
59
93
|
#
|
60
|
-
# @
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
94
|
+
# @return [Failures]
|
95
|
+
#
|
96
|
+
# @see Failures#failure
|
97
|
+
#
|
98
|
+
# @api public
|
99
|
+
def key(path = self.path)
|
100
|
+
(@key ||= EMPTY_HASH.dup)[path] ||= Failures.new(path)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get failures object for base errors
|
65
104
|
#
|
66
|
-
# @
|
67
|
-
# Set message under specified key (overrides rule's default key)
|
68
|
-
# @param id [Symbol] The message key
|
69
|
-
# @example
|
70
|
-
# failure(:my_error, 'this failed')
|
105
|
+
# @return [Failures]
|
71
106
|
#
|
72
|
-
# @
|
107
|
+
# @see Failures#failure
|
73
108
|
#
|
74
109
|
# @api public
|
75
|
-
def
|
76
|
-
|
77
|
-
if args.size.equal?(1)
|
78
|
-
case (msg = args[0])
|
79
|
-
when Symbol
|
80
|
-
[default_id, _context.message(msg, rule: default_id, tokens: tokens)]
|
81
|
-
when String
|
82
|
-
[default_id, msg]
|
83
|
-
end
|
84
|
-
else
|
85
|
-
args
|
86
|
-
end
|
87
|
-
@failure = true
|
88
|
-
@message = [id, text]
|
89
|
-
self
|
110
|
+
def base
|
111
|
+
@base ||= Failures.new
|
90
112
|
end
|
91
113
|
|
92
|
-
#
|
114
|
+
# Return aggregated failures
|
93
115
|
#
|
94
|
-
# @return [
|
116
|
+
# @return [Array<Hash>]
|
95
117
|
#
|
96
118
|
# @api private
|
97
|
-
def
|
98
|
-
|
119
|
+
def failures
|
120
|
+
failures = []
|
121
|
+
failures += @base.opts if defined?(@base)
|
122
|
+
failures.concat(@key.values.flat_map(&:opts)) if defined?(@key)
|
123
|
+
failures
|
99
124
|
end
|
100
125
|
|
101
126
|
# @api private
|
102
127
|
def respond_to_missing?(meth, include_private = false)
|
103
|
-
super ||
|
128
|
+
super || _contract.respond_to?(meth, true)
|
104
129
|
end
|
105
130
|
|
106
131
|
private
|
107
132
|
|
108
|
-
# Forward to the underlying
|
133
|
+
# Forward to the underlying contract
|
109
134
|
#
|
110
135
|
# @api private
|
111
136
|
def method_missing(meth, *args, &block)
|
112
137
|
# yes, we do want to delegate to private methods too
|
113
|
-
if
|
114
|
-
|
138
|
+
if _contract.respond_to?(meth, true)
|
139
|
+
_contract.__send__(meth, *args, &block)
|
115
140
|
else
|
116
141
|
super
|
117
142
|
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Validation
|
5
|
+
module Messages
|
6
|
+
# Resolve translated messages from failure arguments
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
class Resolver
|
10
|
+
# @!attribute [r] messages
|
11
|
+
# @return [Messages::I18n, Messages::YAML] messages backend
|
12
|
+
# @api private
|
13
|
+
attr_reader :messages
|
14
|
+
|
15
|
+
# @!attribute [r] locale
|
16
|
+
# @return [Symbol] current locale
|
17
|
+
# @api private
|
18
|
+
attr_reader :locale
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
def initialize(messages, locale = :en)
|
22
|
+
@messages = messages
|
23
|
+
@locale = locale
|
24
|
+
end
|
25
|
+
|
26
|
+
# Resolve Error object from provided args and path
|
27
|
+
#
|
28
|
+
# This is used internally by contracts when rules are applied
|
29
|
+
#
|
30
|
+
# @return [Error, Error::Localized]
|
31
|
+
#
|
32
|
+
# @api public
|
33
|
+
def call(message:, tokens:, path:)
|
34
|
+
case message
|
35
|
+
when Symbol
|
36
|
+
Error[->(**opts) { message(message, path: path, tokens: tokens, **opts) }, path]
|
37
|
+
when String
|
38
|
+
Error[message, path]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
alias_method :[], :call
|
42
|
+
|
43
|
+
# Resolve a message
|
44
|
+
#
|
45
|
+
# @return [String]
|
46
|
+
#
|
47
|
+
# @api public
|
48
|
+
def message(rule, tokens: EMPTY_HASH, path:, locale: self.locale, full: false)
|
49
|
+
keys = path.to_a.compact
|
50
|
+
msg_opts = tokens.merge(path: keys, locale: locale)
|
51
|
+
|
52
|
+
if keys.empty?
|
53
|
+
template = messages["rules.#{rule}", msg_opts]
|
54
|
+
else
|
55
|
+
template = messages[rule, msg_opts.merge(path: keys.join(DOT))]
|
56
|
+
template ||= messages[rule, msg_opts.merge(path: keys.last)]
|
57
|
+
end
|
58
|
+
|
59
|
+
unless template
|
60
|
+
raise MissingMessageError, <<~STR
|
61
|
+
Message template for #{rule.inspect} under #{keys.join(DOT).inspect} was not found
|
62
|
+
STR
|
63
|
+
end
|
64
|
+
|
65
|
+
text = template.(template.data(tokens))
|
66
|
+
|
67
|
+
full ? "#{messages.rule(keys.last, msg_opts)} #{text}" : text
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'dry/equalizer'
|
4
|
+
|
4
5
|
require 'dry/validation/constants'
|
6
|
+
require 'dry/validation/error_set'
|
5
7
|
|
6
8
|
module Dry
|
7
9
|
module Validation
|
@@ -13,8 +15,10 @@ module Dry
|
|
13
15
|
|
14
16
|
# Build a new result
|
15
17
|
#
|
18
|
+
# @param [Dry::Schema::Result]
|
19
|
+
#
|
16
20
|
# @api private
|
17
|
-
def self.new(
|
21
|
+
def self.new(values, options = EMPTY_HASH)
|
18
22
|
result = super
|
19
23
|
yield(result) if block_given?
|
20
24
|
result.freeze
|
@@ -25,25 +29,29 @@ module Dry
|
|
25
29
|
# @api private
|
26
30
|
attr_reader :values
|
27
31
|
|
28
|
-
# @!attribute [r]
|
29
|
-
# @return [Hash
|
30
|
-
# @api
|
31
|
-
attr_reader :
|
32
|
+
# @!attribute [r] options
|
33
|
+
# @return [Hash]
|
34
|
+
# @api private
|
35
|
+
attr_reader :options
|
32
36
|
|
33
37
|
# Initialize a new result
|
34
38
|
#
|
35
39
|
# @api private
|
36
|
-
def initialize(values,
|
40
|
+
def initialize(values, options)
|
37
41
|
@values = values
|
38
|
-
@
|
42
|
+
@options = options
|
43
|
+
@errors = initialize_errors
|
39
44
|
end
|
40
45
|
|
41
|
-
#
|
46
|
+
# Get error set
|
47
|
+
#
|
48
|
+
# @return [ErrorSet]
|
42
49
|
#
|
43
50
|
# @api public
|
44
|
-
def
|
45
|
-
|
51
|
+
def errors(new_options = EMPTY_HASH)
|
52
|
+
new_options.empty? ? @errors : @errors.with(schema_errors(new_options), new_options)
|
46
53
|
end
|
54
|
+
alias_method :messages, :errors
|
47
55
|
|
48
56
|
# Check if result is successful
|
49
57
|
#
|
@@ -51,7 +59,7 @@ module Dry
|
|
51
59
|
#
|
52
60
|
# @api public
|
53
61
|
def success?
|
54
|
-
errors.empty?
|
62
|
+
@errors.empty?
|
55
63
|
end
|
56
64
|
|
57
65
|
# Check if result is not successful
|
@@ -73,8 +81,8 @@ module Dry
|
|
73
81
|
# Add a new error for the provided key
|
74
82
|
#
|
75
83
|
# @api private
|
76
|
-
def add_error(
|
77
|
-
|
84
|
+
def add_error(error)
|
85
|
+
errors.add(error)
|
78
86
|
self
|
79
87
|
end
|
80
88
|
|
@@ -86,27 +94,7 @@ module Dry
|
|
86
94
|
#
|
87
95
|
# @api public
|
88
96
|
def [](key)
|
89
|
-
|
90
|
-
values[key]
|
91
|
-
elsif storage.key?(key)
|
92
|
-
storage[key]
|
93
|
-
else
|
94
|
-
nil
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
# Store value under specified key
|
99
|
-
#
|
100
|
-
# @param [Symbol] key
|
101
|
-
# @param [Object] value
|
102
|
-
#
|
103
|
-
# @return [Object]
|
104
|
-
#
|
105
|
-
# @api public
|
106
|
-
def []=(key, value)
|
107
|
-
raise ArgumentError, "Key +#{key}+ was already set" if key?(key)
|
108
|
-
|
109
|
-
storage[key] = value
|
97
|
+
values[key]
|
110
98
|
end
|
111
99
|
|
112
100
|
# Check if a key was set
|
@@ -117,7 +105,7 @@ module Dry
|
|
117
105
|
#
|
118
106
|
# @api public
|
119
107
|
def key?(key)
|
120
|
-
values.key?(key)
|
108
|
+
values.key?(key)
|
121
109
|
end
|
122
110
|
|
123
111
|
# Coerce to a hash
|
@@ -126,38 +114,32 @@ module Dry
|
|
126
114
|
def to_h
|
127
115
|
values.to_h
|
128
116
|
end
|
129
|
-
alias_method :to_hash, :to_h
|
130
|
-
|
131
|
-
# Add new errors
|
132
|
-
#
|
133
|
-
# @api private
|
134
|
-
def update(new_errors)
|
135
|
-
errors.update(new_errors)
|
136
|
-
end
|
137
117
|
|
138
118
|
# Return a string representation
|
139
119
|
#
|
140
120
|
# @api public
|
141
121
|
def inspect
|
142
|
-
"#<#{self.class}#{to_h.inspect} errors=#{errors.inspect}>"
|
122
|
+
"#<#{self.class}#{to_h.inspect} errors=#{errors.to_h.inspect}>"
|
123
|
+
end
|
124
|
+
|
125
|
+
# Freeze result and its error set
|
126
|
+
#
|
127
|
+
# @api private
|
128
|
+
def freeze
|
129
|
+
errors.freeze
|
130
|
+
super
|
143
131
|
end
|
144
132
|
|
145
133
|
private
|
146
134
|
|
147
135
|
# @api private
|
148
|
-
def
|
149
|
-
|
136
|
+
def initialize_errors(options = self.options)
|
137
|
+
ErrorSet.new(schema_errors(options), options)
|
150
138
|
end
|
151
139
|
|
152
140
|
# @api private
|
153
|
-
def
|
154
|
-
|
155
|
-
key = path.keys[0]
|
156
|
-
messages = (hash[key] ||= EMPTY_HASH.dup)
|
157
|
-
add_to(messages, path[key], value)
|
158
|
-
else
|
159
|
-
(hash[path] ||= EMPTY_ARRAY.dup) << value
|
160
|
-
end
|
141
|
+
def schema_errors(options)
|
142
|
+
values.message_set(options).to_a
|
161
143
|
end
|
162
144
|
end
|
163
145
|
end
|
data/lib/dry/validation/rule.rb
CHANGED
@@ -25,12 +25,13 @@ module Dry
|
|
25
25
|
|
26
26
|
# Evaluate the rule within the provided context
|
27
27
|
#
|
28
|
-
# @param [Contract]
|
29
|
-
# @param [
|
28
|
+
# @param [Contract] contract
|
29
|
+
# @param [Result] result
|
30
|
+
# @param [Concurrent::Map] context
|
30
31
|
#
|
31
32
|
# @api private
|
32
|
-
def call(
|
33
|
-
Evaluator.new(
|
33
|
+
def call(contract, result, context)
|
34
|
+
Evaluator.new(contract, values: result, keys: keys, _context: context, &block)
|
34
35
|
end
|
35
36
|
end
|
36
37
|
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.
|
4
|
+
version: 1.0.0.beta1
|
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-03-
|
11
|
+
date: 2019-03-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -24,26 +24,6 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: dry-configurable
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '0.1'
|
34
|
-
- - ">="
|
35
|
-
- !ruby/object:Gem::Version
|
36
|
-
version: 0.1.3
|
37
|
-
type: :runtime
|
38
|
-
prerelease: false
|
39
|
-
version_requirements: !ruby/object:Gem::Requirement
|
40
|
-
requirements:
|
41
|
-
- - "~>"
|
42
|
-
- !ruby/object:Gem::Version
|
43
|
-
version: '0.1'
|
44
|
-
- - ">="
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version: 0.1.3
|
47
27
|
- !ruby/object:Gem::Dependency
|
48
28
|
name: dry-core
|
49
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -96,22 +76,22 @@ dependencies:
|
|
96
76
|
name: dry-schema
|
97
77
|
requirement: !ruby/object:Gem::Requirement
|
98
78
|
requirements:
|
99
|
-
- - ">="
|
100
|
-
- !ruby/object:Gem::Version
|
101
|
-
version: '0.3'
|
102
79
|
- - "~>"
|
103
80
|
- !ruby/object:Gem::Version
|
104
81
|
version: '0.3'
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0.4'
|
105
85
|
type: :runtime
|
106
86
|
prerelease: false
|
107
87
|
version_requirements: !ruby/object:Gem::Requirement
|
108
88
|
requirements:
|
109
|
-
- - ">="
|
110
|
-
- !ruby/object:Gem::Version
|
111
|
-
version: '0.3'
|
112
89
|
- - "~>"
|
113
90
|
- !ruby/object:Gem::Version
|
114
91
|
version: '0.3'
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0.4'
|
115
95
|
- !ruby/object:Gem::Dependency
|
116
96
|
name: bundler
|
117
97
|
requirement: !ruby/object:Gem::Requirement
|
@@ -166,12 +146,15 @@ files:
|
|
166
146
|
- README.md
|
167
147
|
- lib/dry-validation.rb
|
168
148
|
- lib/dry/validation.rb
|
149
|
+
- lib/dry/validation/config.rb
|
169
150
|
- lib/dry/validation/constants.rb
|
170
151
|
- lib/dry/validation/contract.rb
|
171
152
|
- lib/dry/validation/contract/class_interface.rb
|
153
|
+
- lib/dry/validation/error.rb
|
154
|
+
- lib/dry/validation/error_set.rb
|
172
155
|
- lib/dry/validation/evaluator.rb
|
173
156
|
- lib/dry/validation/extensions/monads.rb
|
174
|
-
- lib/dry/validation/messages.rb
|
157
|
+
- lib/dry/validation/messages/resolver.rb
|
175
158
|
- lib/dry/validation/result.rb
|
176
159
|
- lib/dry/validation/rule.rb
|
177
160
|
- lib/dry/validation/version.rb
|
@@ -194,7 +177,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
194
177
|
- !ruby/object:Gem::Version
|
195
178
|
version: 1.3.1
|
196
179
|
requirements: []
|
197
|
-
rubygems_version: 3.0.
|
180
|
+
rubygems_version: 3.0.3
|
198
181
|
signing_key:
|
199
182
|
specification_version: 4
|
200
183
|
summary: Validation library
|
@@ -1,58 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'dry/schema/messages'
|
4
|
-
|
5
|
-
module Dry
|
6
|
-
module Validation
|
7
|
-
module Messages
|
8
|
-
class YAML < Schema::Messages::YAML
|
9
|
-
config.root = config.root.gsub('dry_schema', 'dry_validation')
|
10
|
-
config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
|
11
|
-
path.gsub('dry_schema', 'dry_validation')
|
12
|
-
}
|
13
|
-
end
|
14
|
-
|
15
|
-
if defined?(::I18n)
|
16
|
-
class I18n < Schema::Messages::I18n
|
17
|
-
config.root = config.root.gsub('dry_schema', 'dry_validation')
|
18
|
-
config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
|
19
|
-
path.gsub('dry_schema', 'dry_validation')
|
20
|
-
}
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
# @api private
|
25
|
-
def self.setup(config)
|
26
|
-
messages = build(config)
|
27
|
-
|
28
|
-
if config.messages_file && config.namespace
|
29
|
-
messages.merge(config.messages_file).namespaced(config.namespace)
|
30
|
-
elsif config.messages_file
|
31
|
-
messages.merge(config.messages_file)
|
32
|
-
elsif config.namespace
|
33
|
-
messages.namespaced(config.namespace)
|
34
|
-
else
|
35
|
-
messages
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# @api private
|
40
|
-
def self.build(config)
|
41
|
-
klass =
|
42
|
-
case config.messages
|
43
|
-
when :yaml then default
|
44
|
-
when :i18n then Messages::I18n
|
45
|
-
else
|
46
|
-
raise "+#{config.messages}+ is not a valid messages identifier"
|
47
|
-
end
|
48
|
-
|
49
|
-
klass.build
|
50
|
-
end
|
51
|
-
|
52
|
-
# @api private
|
53
|
-
def self.default
|
54
|
-
Messages::YAML
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|