dry-schema 0.1.0 → 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 +166 -0
- data/LICENSE +1 -1
- data/README.md +13 -4
- data/config/errors.yml +59 -54
- data/lib/dry/schema/compiler.rb +24 -0
- data/lib/dry/schema/config.rb +23 -27
- data/lib/dry/schema/constants.rb +27 -3
- data/lib/dry/schema/dsl.rb +36 -49
- data/lib/dry/schema/extensions/hints/compiler_methods.rb +19 -0
- data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +84 -0
- data/lib/dry/schema/extensions/hints/message_set_methods.rb +44 -0
- data/lib/dry/schema/extensions/hints/result_methods.rb +47 -0
- data/lib/dry/schema/extensions/hints.rb +56 -0
- data/lib/dry/schema/extensions/monads.rb +15 -4
- data/lib/dry/schema/extensions.rb +6 -0
- data/lib/dry/schema/json.rb +4 -2
- data/lib/dry/schema/key.rb +8 -10
- data/lib/dry/schema/key_coercer.rb +6 -2
- data/lib/dry/schema/key_map.rb +5 -4
- data/lib/dry/schema/macros/array.rb +39 -0
- data/lib/dry/schema/macros/core.rb +2 -0
- data/lib/dry/schema/macros/dsl.rb +149 -4
- data/lib/dry/schema/macros/each.rb +12 -1
- data/lib/dry/schema/macros/filled.rb +14 -7
- data/lib/dry/schema/macros/hash.rb +6 -32
- data/lib/dry/schema/macros/key.rb +38 -60
- data/lib/dry/schema/macros/maybe.rb +5 -3
- data/lib/dry/schema/macros/optional.rb +8 -1
- data/lib/dry/schema/macros/required.rb +3 -1
- data/lib/dry/schema/macros/schema.rb +46 -0
- data/lib/dry/schema/macros/value.rb +18 -2
- data/lib/dry/schema/macros.rb +4 -0
- data/lib/dry/schema/message/or.rb +54 -0
- data/lib/dry/schema/message.rb +56 -72
- data/lib/dry/schema/message_compiler/visitor_opts.rb +28 -1
- data/lib/dry/schema/message_compiler.rb +85 -74
- data/lib/dry/schema/message_set.rb +82 -50
- data/lib/dry/schema/messages/abstract.rb +84 -44
- data/lib/dry/schema/messages/i18n.rb +45 -9
- data/lib/dry/schema/messages/namespaced.rb +22 -5
- data/lib/dry/schema/messages/template.rb +68 -0
- data/lib/dry/schema/messages/yaml.rb +76 -22
- data/lib/dry/schema/messages.rb +19 -24
- data/lib/dry/schema/namespaced_rule.rb +37 -0
- data/lib/dry/schema/params.rb +4 -2
- data/lib/dry/schema/path.rb +102 -0
- data/lib/dry/schema/predicate.rb +15 -0
- data/lib/dry/schema/predicate_inferrer.rb +116 -23
- data/lib/dry/schema/predicate_registry.rb +2 -0
- data/lib/dry/schema/processor.rb +70 -39
- data/lib/dry/schema/result.rb +16 -31
- data/lib/dry/schema/rule_applier.rb +10 -3
- data/lib/dry/schema/trace.rb +16 -3
- data/lib/dry/schema/type_registry.rb +5 -3
- data/lib/dry/schema/types.rb +3 -1
- data/lib/dry/schema/value_coercer.rb +10 -4
- data/lib/dry/schema/version.rb +3 -1
- data/lib/dry/schema.rb +39 -13
- data/lib/dry-schema.rb +2 -0
- metadata +43 -47
data/lib/dry/schema/message.rb
CHANGED
|
@@ -1,102 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry/initializer'
|
|
1
4
|
require 'dry/equalizer'
|
|
2
5
|
|
|
6
|
+
require 'dry/schema/path'
|
|
7
|
+
require 'dry/schema/message/or'
|
|
8
|
+
|
|
3
9
|
module Dry
|
|
4
10
|
module Schema
|
|
5
11
|
# Message objects used by message sets
|
|
6
12
|
#
|
|
7
13
|
# @api public
|
|
8
14
|
class Message
|
|
9
|
-
include Dry::Equalizer(:
|
|
15
|
+
include Dry::Equalizer(:text, :path, :predicate, :input)
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
# A message sub-type used by OR operations
|
|
14
|
-
#
|
|
15
|
-
# @api public
|
|
16
|
-
class Or
|
|
17
|
-
# @api private
|
|
18
|
-
attr_reader :left
|
|
17
|
+
extend Dry::Initializer
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
# @!attribute [r] text
|
|
20
|
+
# Message text representation created from a localized template
|
|
21
|
+
# @return [String]
|
|
22
|
+
option :text
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
# @!attribute [r] path
|
|
25
|
+
# Path to the value
|
|
26
|
+
# @return [String]
|
|
27
|
+
option :path
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
# @!attribute [r] predicate
|
|
30
|
+
# Predicate identifier that was used to produce a message
|
|
31
|
+
# @return [Symbol]
|
|
32
|
+
option :predicate
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@messages = messages
|
|
34
|
-
@path = left.path
|
|
35
|
-
end
|
|
34
|
+
# @!attribute [r] args
|
|
35
|
+
# Optional list of arguments used by the predicate
|
|
36
|
+
# @return [Array]
|
|
37
|
+
option :args, default: proc { EMPTY_ARRAY }
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
# @!attribute [r] input
|
|
40
|
+
# The input value
|
|
41
|
+
# @return [Object]
|
|
42
|
+
option :input
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
[left, right].uniq.join(" #{messages[:or]} ")
|
|
47
|
-
end
|
|
48
|
-
end
|
|
44
|
+
# @!attribute [r] meta
|
|
45
|
+
# Arbitrary meta data
|
|
46
|
+
# @return [Hash]
|
|
47
|
+
option :meta, optional: true, default: proc { EMPTY_HASH }
|
|
49
48
|
|
|
50
|
-
#
|
|
49
|
+
# Dump the message to a representation suitable for the message set hash
|
|
51
50
|
#
|
|
52
|
-
# @
|
|
53
|
-
def self.[](predicate, path, text, options)
|
|
54
|
-
Message.new(predicate, path, text, options)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# @api private
|
|
58
|
-
def initialize(predicate, path, text, options)
|
|
59
|
-
@predicate = predicate
|
|
60
|
-
@path = path.dup
|
|
61
|
-
@text = text
|
|
62
|
-
@options = options
|
|
63
|
-
@rule = options[:rule]
|
|
64
|
-
@args = options[:args] || EMPTY_ARRAY
|
|
65
|
-
|
|
66
|
-
if predicate == :key?
|
|
67
|
-
@path << rule
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Return a string representation of the message
|
|
51
|
+
# @return [String,Hash]
|
|
72
52
|
#
|
|
73
53
|
# @api public
|
|
74
|
-
def
|
|
75
|
-
text
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# @api private
|
|
79
|
-
def hint?
|
|
80
|
-
false
|
|
54
|
+
def dump
|
|
55
|
+
@dump ||= meta.empty? ? text : { text: text, **meta }
|
|
81
56
|
end
|
|
57
|
+
alias to_s dump
|
|
82
58
|
|
|
59
|
+
# See if another message is the same
|
|
60
|
+
#
|
|
61
|
+
# If a string is passed, it will be compared with the text
|
|
62
|
+
#
|
|
63
|
+
# @param [Message,String]
|
|
64
|
+
#
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
#
|
|
83
67
|
# @api private
|
|
84
68
|
def eql?(other)
|
|
85
69
|
other.is_a?(String) ? text == other : super
|
|
86
70
|
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# A hint message sub-type
|
|
90
|
-
#
|
|
91
|
-
# @api private
|
|
92
|
-
class Hint < Message
|
|
93
|
-
def self.[](predicate, path, text, options)
|
|
94
|
-
Hint.new(predicate, path, text, options)
|
|
95
|
-
end
|
|
96
71
|
|
|
72
|
+
# See which message is higher in the hierarchy
|
|
73
|
+
#
|
|
97
74
|
# @api private
|
|
98
|
-
def
|
|
99
|
-
|
|
75
|
+
def <=>(other)
|
|
76
|
+
l_path = Path[path]
|
|
77
|
+
r_path = Path[other.path]
|
|
78
|
+
|
|
79
|
+
unless l_path.same_root?(r_path)
|
|
80
|
+
raise ArgumentError, 'Cannot compare messages from different root paths'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
l_path <=> r_path
|
|
100
84
|
end
|
|
101
85
|
end
|
|
102
86
|
end
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry/schema/constants'
|
|
4
|
+
require 'dry/schema/message'
|
|
5
|
+
|
|
1
6
|
module Dry
|
|
2
7
|
module Schema
|
|
3
8
|
# @api private
|
|
@@ -10,8 +15,8 @@ module Dry
|
|
|
10
15
|
def self.new
|
|
11
16
|
opts = super
|
|
12
17
|
opts[:path] = EMPTY_ARRAY
|
|
13
|
-
opts[:rule] = nil
|
|
14
18
|
opts[:message_type] = :failure
|
|
19
|
+
opts[:current_messages] = EMPTY_ARRAY.dup
|
|
15
20
|
opts
|
|
16
21
|
end
|
|
17
22
|
|
|
@@ -24,6 +29,28 @@ module Dry
|
|
|
24
29
|
def call(other)
|
|
25
30
|
merge(other.update(path: [*path, *other[:path]]))
|
|
26
31
|
end
|
|
32
|
+
|
|
33
|
+
def dup(current_messages = EMPTY_ARRAY.dup)
|
|
34
|
+
opts = super()
|
|
35
|
+
opts[:current_messages] = current_messages
|
|
36
|
+
opts
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def key_failure?(path)
|
|
40
|
+
failures.any? { |f| f.path == path && f.predicate.equal?(:key?) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def failures
|
|
44
|
+
current_messages.reject(&:hint?)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def hints
|
|
48
|
+
current_messages.select(&:hint?)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def current_messages
|
|
52
|
+
self[:current_messages]
|
|
53
|
+
end
|
|
27
54
|
end
|
|
28
55
|
end
|
|
29
56
|
end
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry/initializer'
|
|
4
|
+
|
|
1
5
|
require 'dry/schema/constants'
|
|
2
6
|
require 'dry/schema/message'
|
|
3
7
|
require 'dry/schema/message_set'
|
|
@@ -9,73 +13,84 @@ module Dry
|
|
|
9
13
|
#
|
|
10
14
|
# @api private
|
|
11
15
|
class MessageCompiler
|
|
12
|
-
|
|
16
|
+
extend Dry::Initializer
|
|
17
|
+
|
|
18
|
+
resolve_key_predicate = proc { |node, opts|
|
|
19
|
+
*arg_vals, val = node.map(&:last)
|
|
20
|
+
[[*opts.path, arg_vals[0]], arg_vals[1..arg_vals.size - 1], val]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resolve_predicate = proc { |node, opts|
|
|
24
|
+
[Array(opts.path), *node.map(&:last)]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
DEFAULT_PREDICATE_RESOLVERS = Hash
|
|
28
|
+
.new(resolve_predicate).update(key?: resolve_key_predicate).freeze
|
|
13
29
|
|
|
14
30
|
EMPTY_OPTS = VisitorOpts.new
|
|
15
|
-
|
|
31
|
+
EMPTY_MESSAGE_SET = MessageSet.new(EMPTY_ARRAY).freeze
|
|
16
32
|
|
|
17
|
-
|
|
18
|
-
def initialize(messages, options = {})
|
|
19
|
-
@messages = messages
|
|
20
|
-
@options = options
|
|
21
|
-
@full = @options.fetch(:full, false)
|
|
22
|
-
@hints = @options.fetch(:hints, true)
|
|
23
|
-
@locale = @options.fetch(:locale, messages.default_locale)
|
|
24
|
-
@default_lookup_options = { locale: locale }
|
|
25
|
-
end
|
|
33
|
+
param :messages
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
option :full, default: -> { false }
|
|
36
|
+
option :locale, default: -> { :en }
|
|
37
|
+
option :predicate_resolvers, default: -> { DEFAULT_PREDICATE_RESOLVERS }
|
|
38
|
+
|
|
39
|
+
attr_reader :options
|
|
40
|
+
|
|
41
|
+
attr_reader :default_lookup_options
|
|
31
42
|
|
|
32
43
|
# @api private
|
|
33
|
-
def
|
|
34
|
-
|
|
44
|
+
def initialize(messages, options = EMPTY_HASH)
|
|
45
|
+
super
|
|
46
|
+
@options = options
|
|
47
|
+
@default_lookup_options = options[:locale] ? { locale: locale } : EMPTY_HASH
|
|
35
48
|
end
|
|
36
49
|
|
|
37
50
|
# @api private
|
|
38
51
|
def with(new_options)
|
|
39
52
|
return self if new_options.empty?
|
|
40
|
-
|
|
53
|
+
|
|
54
|
+
updated_opts = options.merge(new_options)
|
|
55
|
+
|
|
56
|
+
return self if updated_opts.eql?(options)
|
|
57
|
+
|
|
58
|
+
self.class.new(messages, updated_opts)
|
|
41
59
|
end
|
|
42
60
|
|
|
43
61
|
# @api private
|
|
44
62
|
def call(ast)
|
|
45
|
-
|
|
63
|
+
return EMPTY_MESSAGE_SET if ast.empty?
|
|
64
|
+
|
|
65
|
+
current_messages = EMPTY_ARRAY.dup
|
|
66
|
+
compiled_messages = ast.map { |node| visit(node, EMPTY_OPTS.dup(current_messages)) }
|
|
67
|
+
|
|
68
|
+
MessageSet[compiled_messages, failures: options.fetch(:failures, true)]
|
|
46
69
|
end
|
|
47
70
|
|
|
48
71
|
# @api private
|
|
49
|
-
def visit(node,
|
|
50
|
-
__send__(:"visit_#{node[0]}", node[1],
|
|
72
|
+
def visit(node, opts = EMPTY_OPTS.dup)
|
|
73
|
+
__send__(:"visit_#{node[0]}", node[1], opts)
|
|
51
74
|
end
|
|
52
75
|
|
|
53
76
|
# @api private
|
|
54
|
-
def visit_failure(node, opts
|
|
77
|
+
def visit_failure(node, opts)
|
|
55
78
|
rule, other = node
|
|
56
79
|
visit(other, opts.(rule: rule))
|
|
57
80
|
end
|
|
58
81
|
|
|
59
82
|
# @api private
|
|
60
|
-
def visit_hint(
|
|
61
|
-
|
|
62
|
-
visit(node, opts.(message_type: :hint))
|
|
63
|
-
end
|
|
83
|
+
def visit_hint(*)
|
|
84
|
+
nil
|
|
64
85
|
end
|
|
65
86
|
|
|
66
87
|
# @api private
|
|
67
|
-
def
|
|
68
|
-
# TODO: we can still generate a hint for elements here!
|
|
69
|
-
[]
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# @api private
|
|
73
|
-
def visit_not(node, opts = EMPTY_OPTS.dup)
|
|
88
|
+
def visit_not(node, opts)
|
|
74
89
|
visit(node, opts.(not: true))
|
|
75
90
|
end
|
|
76
91
|
|
|
77
92
|
# @api private
|
|
78
|
-
def visit_and(node, opts
|
|
93
|
+
def visit_and(node, opts)
|
|
79
94
|
left, right = node.map { |n| visit(n, opts) }
|
|
80
95
|
|
|
81
96
|
if right
|
|
@@ -86,59 +101,57 @@ module Dry
|
|
|
86
101
|
end
|
|
87
102
|
|
|
88
103
|
# @api private
|
|
89
|
-
def visit_or(node, opts
|
|
104
|
+
def visit_or(node, opts)
|
|
90
105
|
left, right = node.map { |n| visit(n, opts) }
|
|
91
106
|
|
|
92
107
|
if [left, right].flatten.map(&:path).uniq.size == 1
|
|
93
|
-
Message::Or.new(left, right,
|
|
108
|
+
Message::Or.new(left, right, proc { |k| messages.translate(k, default_lookup_options) })
|
|
94
109
|
elsif right.is_a?(Array)
|
|
95
110
|
right
|
|
96
111
|
else
|
|
97
|
-
[left, right]
|
|
112
|
+
[left, right].flatten.max
|
|
98
113
|
end
|
|
99
114
|
end
|
|
100
115
|
|
|
101
116
|
# @api private
|
|
102
|
-
def
|
|
117
|
+
def visit_namespace(node, opts)
|
|
118
|
+
ns, rest = node
|
|
119
|
+
self.class.new(messages.namespaced(ns), options).visit(rest, opts)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @api private
|
|
123
|
+
def visit_predicate(node, opts)
|
|
103
124
|
predicate, args = node
|
|
104
125
|
|
|
105
|
-
*arg_vals, val = args.map(&:last)
|
|
106
126
|
tokens = message_tokens(args)
|
|
127
|
+
path, *arg_vals, input = predicate_resolvers[predicate].(args, opts)
|
|
107
128
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
msg_opts = options.update(tokens)
|
|
112
|
-
|
|
113
|
-
rule = msg_opts[:rule]
|
|
114
|
-
path = msg_opts[:path]
|
|
129
|
+
options = opts.dup.update(
|
|
130
|
+
path: path.last, **tokens, **lookup_options(arg_vals: arg_vals, input: input)
|
|
131
|
+
).to_h
|
|
115
132
|
|
|
116
|
-
template = messages[
|
|
133
|
+
template, meta = messages[predicate, options] || raise(MissingMessageError, path)
|
|
117
134
|
|
|
118
|
-
|
|
119
|
-
raise MissingMessageError, "message for #{predicate} was not found"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
text = message_text(rule, template, tokens, options)
|
|
135
|
+
text = message_text(template, tokens, options)
|
|
123
136
|
|
|
124
|
-
|
|
137
|
+
message_type(options).new(
|
|
138
|
+
text: text, path: path, predicate: predicate, args: arg_vals, input: input, meta: meta
|
|
139
|
+
)
|
|
140
|
+
end
|
|
125
141
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
input: input,
|
|
130
|
-
rule: rule || msg_opts[:name]
|
|
131
|
-
]
|
|
142
|
+
# @api private
|
|
143
|
+
def message_type(*)
|
|
144
|
+
Message
|
|
132
145
|
end
|
|
133
146
|
|
|
134
147
|
# @api private
|
|
135
|
-
def visit_key(node, opts
|
|
148
|
+
def visit_key(node, opts)
|
|
136
149
|
name, other = node
|
|
137
150
|
visit(other, opts.(path: name))
|
|
138
151
|
end
|
|
139
152
|
|
|
140
153
|
# @api private
|
|
141
|
-
def visit_set(node, opts
|
|
154
|
+
def visit_set(node, opts)
|
|
142
155
|
node.map { |el| visit(el, opts) }
|
|
143
156
|
end
|
|
144
157
|
|
|
@@ -149,34 +162,32 @@ module Dry
|
|
|
149
162
|
end
|
|
150
163
|
|
|
151
164
|
# @api private
|
|
152
|
-
def visit_xor(node, opts
|
|
165
|
+
def visit_xor(node, opts)
|
|
153
166
|
left, right = node
|
|
154
167
|
[visit(left, opts), visit(right, opts)].uniq
|
|
155
168
|
end
|
|
156
169
|
|
|
157
170
|
# @api private
|
|
158
|
-
def lookup_options(arg_vals
|
|
171
|
+
def lookup_options(arg_vals:, input:)
|
|
159
172
|
default_lookup_options.merge(
|
|
160
173
|
arg_type: arg_vals.size == 1 && arg_vals[0].class,
|
|
161
|
-
val_type: input.class
|
|
174
|
+
val_type: input.equal?(Undefined) ? NilClass : input.class
|
|
162
175
|
)
|
|
163
176
|
end
|
|
164
177
|
|
|
165
178
|
# @api private
|
|
166
|
-
def message_text(
|
|
167
|
-
text = template
|
|
179
|
+
def message_text(template, tokens, options)
|
|
180
|
+
text = template[template.data(tokens)]
|
|
168
181
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
text
|
|
174
|
-
end
|
|
182
|
+
return text unless full
|
|
183
|
+
|
|
184
|
+
rule = options[:path]
|
|
185
|
+
"#{messages.rule(rule, options) || rule} #{text}"
|
|
175
186
|
end
|
|
176
187
|
|
|
177
188
|
# @api private
|
|
178
189
|
def message_tokens(args)
|
|
179
|
-
args.each_with_object({})
|
|
190
|
+
args.each_with_object({}) do |arg, hash|
|
|
180
191
|
case arg[1]
|
|
181
192
|
when Array
|
|
182
193
|
hash[arg[0]] = arg[1].join(LIST_SEPARATOR)
|
|
@@ -186,7 +197,7 @@ module Dry
|
|
|
186
197
|
else
|
|
187
198
|
hash[arg[0]] = arg[1]
|
|
188
199
|
end
|
|
189
|
-
|
|
200
|
+
end
|
|
190
201
|
end
|
|
191
202
|
end
|
|
192
203
|
end
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry/equalizer'
|
|
4
|
+
|
|
1
5
|
module Dry
|
|
2
6
|
module Schema
|
|
3
7
|
# A set of messages used to generate errors
|
|
@@ -7,15 +11,23 @@ module Dry
|
|
|
7
11
|
# @api public
|
|
8
12
|
class MessageSet
|
|
9
13
|
include Enumerable
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
include Dry::Equalizer(:messages, :options)
|
|
15
|
+
|
|
16
|
+
# A list of compiled message objects
|
|
17
|
+
#
|
|
18
|
+
# @return [Array<Message>]
|
|
19
|
+
attr_reader :messages
|
|
20
|
+
|
|
21
|
+
# An internal hash that is filled in with dumped messages
|
|
22
|
+
# when a message set is coerced to a hash
|
|
23
|
+
#
|
|
24
|
+
# @return [Hash<Symbol=>[Array,Hash]>]
|
|
25
|
+
attr_reader :placeholders
|
|
26
|
+
|
|
27
|
+
# Options hash
|
|
28
|
+
#
|
|
29
|
+
# @return [Hash]
|
|
30
|
+
attr_reader :options
|
|
19
31
|
|
|
20
32
|
# @api private
|
|
21
33
|
def self.[](messages, options = EMPTY_HASH)
|
|
@@ -25,91 +37,111 @@ module Dry
|
|
|
25
37
|
# @api private
|
|
26
38
|
def initialize(messages, options = EMPTY_HASH)
|
|
27
39
|
@messages = messages
|
|
28
|
-
@hints = messages.select(&:hint?)
|
|
29
|
-
@failures = messages - hints
|
|
30
|
-
@paths = failures.map(&:path).uniq
|
|
31
40
|
@options = options
|
|
32
|
-
|
|
33
|
-
initialize_hints!
|
|
34
41
|
initialize_placeholders!
|
|
35
42
|
end
|
|
36
43
|
|
|
44
|
+
# Iterate over messages
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# result.errors.each do |message|
|
|
48
|
+
# puts message.text
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# @return [Array]
|
|
52
|
+
#
|
|
37
53
|
# @api public
|
|
38
54
|
def each(&block)
|
|
55
|
+
return self if empty?
|
|
39
56
|
return to_enum unless block
|
|
57
|
+
|
|
40
58
|
messages.each(&block)
|
|
41
59
|
end
|
|
42
60
|
|
|
61
|
+
# Dump message set to a hash
|
|
62
|
+
#
|
|
63
|
+
# @return [Hash<Symbol=>Array<String>>]
|
|
64
|
+
#
|
|
43
65
|
# @api public
|
|
44
66
|
def to_h
|
|
45
|
-
|
|
67
|
+
@to_h ||= messages_map
|
|
46
68
|
end
|
|
47
69
|
alias_method :to_hash, :to_h
|
|
48
|
-
alias_method :dump, :to_h
|
|
49
70
|
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
# Get a list of message texts for the given key
|
|
72
|
+
#
|
|
73
|
+
# @param [Symbol] key
|
|
74
|
+
#
|
|
75
|
+
# @return [Array<String>]
|
|
76
|
+
#
|
|
77
|
+
# @api public
|
|
78
|
+
def [](key)
|
|
79
|
+
to_h[key]
|
|
53
80
|
end
|
|
54
81
|
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
82
|
+
# Get a list of message texts for the given key
|
|
83
|
+
#
|
|
84
|
+
# @param [Symbol] key
|
|
85
|
+
#
|
|
86
|
+
# @return [Array<String>]
|
|
87
|
+
#
|
|
88
|
+
# @raise KeyError
|
|
89
|
+
#
|
|
90
|
+
# @api public
|
|
91
|
+
def fetch(key)
|
|
92
|
+
self[key] || raise(KeyError, "+#{key}+ message was not found")
|
|
58
93
|
end
|
|
59
94
|
|
|
60
|
-
|
|
95
|
+
# Check if a message set is empty
|
|
96
|
+
#
|
|
97
|
+
# @return [Boolean]
|
|
98
|
+
#
|
|
99
|
+
# @api public
|
|
100
|
+
def empty?
|
|
101
|
+
@empty ||= messages.empty?
|
|
102
|
+
end
|
|
61
103
|
|
|
62
104
|
# @api private
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
msgs.each do |msg|
|
|
68
|
-
node << msg
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
msg_hints = hint_groups[path]
|
|
72
|
-
node.concat(msg_hints) if msg_hints
|
|
73
|
-
|
|
74
|
-
node.map!(&:to_s)
|
|
75
|
-
|
|
76
|
-
hash
|
|
77
|
-
end
|
|
105
|
+
def freeze
|
|
106
|
+
to_h
|
|
107
|
+
empty?
|
|
108
|
+
super
|
|
78
109
|
end
|
|
79
110
|
|
|
111
|
+
private
|
|
112
|
+
|
|
80
113
|
# @api private
|
|
81
|
-
def
|
|
82
|
-
|
|
114
|
+
def messages_map(messages = self.messages)
|
|
115
|
+
return EMPTY_HASH if empty?
|
|
116
|
+
|
|
117
|
+
messages.group_by(&:path).reduce(placeholders) do |hash, (path, msgs)|
|
|
83
118
|
node = path.reduce(hash) { |a, e| a[e] }
|
|
84
119
|
|
|
85
120
|
msgs.each do |msg|
|
|
86
121
|
node << msg
|
|
87
122
|
end
|
|
88
123
|
|
|
89
|
-
node.map!(&:
|
|
124
|
+
node.map!(&:dump)
|
|
90
125
|
|
|
91
126
|
hash
|
|
92
127
|
end
|
|
93
128
|
end
|
|
94
129
|
|
|
95
130
|
# @api private
|
|
96
|
-
def
|
|
97
|
-
@
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
# @api private
|
|
101
|
-
def initialize_hints!
|
|
102
|
-
hints.reject! { |hint| HINT_EXCLUSION.include?(hint.predicate) }
|
|
131
|
+
def paths
|
|
132
|
+
@paths ||= messages.map(&:path).uniq
|
|
103
133
|
end
|
|
104
134
|
|
|
105
135
|
# @api private
|
|
106
136
|
def initialize_placeholders!
|
|
107
|
-
@placeholders =
|
|
137
|
+
return @placeholders = EMPTY_HASH if empty?
|
|
138
|
+
|
|
139
|
+
@placeholders = paths.reduce(EMPTY_HASH.dup) do |hash, path|
|
|
108
140
|
curr_idx = 0
|
|
109
141
|
last_idx = path.size - 1
|
|
110
142
|
node = hash
|
|
111
143
|
|
|
112
|
-
while curr_idx <= last_idx
|
|
144
|
+
while curr_idx <= last_idx
|
|
113
145
|
key = path[curr_idx]
|
|
114
146
|
node = (node[key] || node[key] = curr_idx < last_idx ? {} : [])
|
|
115
147
|
curr_idx += 1
|