dry-schema 1.4.1 → 1.5.2
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 +210 -73
- data/LICENSE +1 -1
- data/README.md +4 -6
- data/config/errors.yml +4 -0
- data/dry-schema.gemspec +46 -0
- data/lib/dry-schema.rb +1 -1
- data/lib/dry/schema.rb +20 -7
- data/lib/dry/schema/compiler.rb +5 -5
- data/lib/dry/schema/config.rb +15 -6
- data/lib/dry/schema/constants.rb +16 -7
- data/lib/dry/schema/dsl.rb +89 -31
- data/lib/dry/schema/extensions.rb +10 -2
- data/lib/dry/schema/extensions/hints.rb +15 -8
- data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +2 -2
- data/lib/dry/schema/extensions/hints/message_set_methods.rb +0 -47
- data/lib/dry/schema/extensions/info.rb +27 -0
- data/lib/dry/schema/extensions/info/schema_compiler.rb +105 -0
- data/lib/dry/schema/extensions/monads.rb +1 -1
- data/lib/dry/schema/extensions/struct.rb +32 -0
- data/lib/dry/schema/json.rb +1 -1
- data/lib/dry/schema/key.rb +20 -5
- data/lib/dry/schema/key_coercer.rb +4 -4
- data/lib/dry/schema/key_map.rb +9 -4
- data/lib/dry/schema/key_validator.rb +67 -0
- data/lib/dry/schema/macros.rb +8 -8
- data/lib/dry/schema/macros/array.rb +17 -4
- data/lib/dry/schema/macros/core.rb +11 -6
- data/lib/dry/schema/macros/dsl.rb +44 -23
- data/lib/dry/schema/macros/each.rb +4 -4
- data/lib/dry/schema/macros/filled.rb +5 -5
- data/lib/dry/schema/macros/hash.rb +21 -3
- data/lib/dry/schema/macros/key.rb +10 -9
- data/lib/dry/schema/macros/maybe.rb +4 -5
- data/lib/dry/schema/macros/optional.rb +1 -1
- data/lib/dry/schema/macros/required.rb +1 -1
- data/lib/dry/schema/macros/schema.rb +23 -2
- data/lib/dry/schema/macros/value.rb +34 -7
- data/lib/dry/schema/message.rb +35 -9
- data/lib/dry/schema/message/or.rb +18 -39
- data/lib/dry/schema/message/or/abstract.rb +28 -0
- data/lib/dry/schema/message/or/multi_path.rb +37 -0
- data/lib/dry/schema/message/or/single_path.rb +64 -0
- data/lib/dry/schema/message_compiler.rb +58 -22
- data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
- data/lib/dry/schema/message_set.rb +26 -37
- data/lib/dry/schema/messages.rb +6 -6
- data/lib/dry/schema/messages/abstract.rb +54 -62
- data/lib/dry/schema/messages/i18n.rb +36 -10
- data/lib/dry/schema/messages/namespaced.rb +12 -2
- data/lib/dry/schema/messages/template.rb +19 -44
- data/lib/dry/schema/messages/yaml.rb +61 -14
- data/lib/dry/schema/params.rb +1 -1
- data/lib/dry/schema/path.rb +44 -5
- data/lib/dry/schema/predicate.rb +4 -2
- data/lib/dry/schema/predicate_inferrer.rb +4 -184
- data/lib/dry/schema/predicate_registry.rb +2 -2
- data/lib/dry/schema/primitive_inferrer.rb +16 -0
- data/lib/dry/schema/processor.rb +50 -29
- data/lib/dry/schema/processor_steps.rb +50 -27
- data/lib/dry/schema/result.rb +53 -6
- data/lib/dry/schema/rule_applier.rb +7 -7
- data/lib/dry/schema/step.rb +79 -0
- data/lib/dry/schema/trace.rb +5 -4
- data/lib/dry/schema/type_container.rb +3 -3
- data/lib/dry/schema/type_registry.rb +2 -2
- data/lib/dry/schema/types.rb +1 -1
- data/lib/dry/schema/value_coercer.rb +2 -2
- data/lib/dry/schema/version.rb +1 -1
- metadata +21 -7
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/equalizer"
|
4
4
|
|
5
5
|
module Dry
|
6
6
|
module Schema
|
@@ -17,13 +17,7 @@ module Dry
|
|
17
17
|
#
|
18
18
|
# @return [Array<Message>]
|
19
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
|
-
|
20
|
+
|
27
21
|
# Options hash
|
28
22
|
#
|
29
23
|
# @return [Hash]
|
@@ -38,7 +32,6 @@ module Dry
|
|
38
32
|
def initialize(messages, options = EMPTY_HASH)
|
39
33
|
@messages = messages
|
40
34
|
@options = options
|
41
|
-
initialize_placeholders!
|
42
35
|
end
|
43
36
|
|
44
37
|
# Iterate over messages
|
@@ -112,43 +105,39 @@ module Dry
|
|
112
105
|
|
113
106
|
# @api private
|
114
107
|
def messages_map(messages = self.messages)
|
115
|
-
|
116
|
-
|
117
|
-
messages.group_by(&:path).reduce(placeholders) do |hash, (path, msgs)|
|
118
|
-
node = path.reduce(hash) { |a, e| a[e] }
|
108
|
+
combine_message_hashes(messages.map(&:to_h))
|
109
|
+
end
|
119
110
|
|
120
|
-
|
121
|
-
|
111
|
+
# @api private
|
112
|
+
def combine_message_hashes(hashes)
|
113
|
+
hashes.reduce(EMPTY_HASH.dup) do |a, e|
|
114
|
+
a.merge(e) do |_, *values|
|
115
|
+
combine_message_values(values)
|
122
116
|
end
|
123
|
-
|
124
|
-
node.map!(&:dump)
|
125
|
-
|
126
|
-
hash
|
127
117
|
end
|
128
118
|
end
|
129
119
|
|
130
120
|
# @api private
|
131
|
-
def
|
132
|
-
|
121
|
+
def combine_message_values(values)
|
122
|
+
hashes, other = partition_message_values(values)
|
123
|
+
combined = combine_message_hashes(hashes)
|
124
|
+
flattened = other.flatten
|
125
|
+
|
126
|
+
if flattened.empty?
|
127
|
+
combined
|
128
|
+
elsif combined.empty?
|
129
|
+
flattened
|
130
|
+
else
|
131
|
+
[flattened, combined]
|
132
|
+
end
|
133
133
|
end
|
134
134
|
|
135
135
|
# @api private
|
136
|
-
def
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
last_idx = path.size - 1
|
142
|
-
node = hash
|
143
|
-
|
144
|
-
while curr_idx <= last_idx
|
145
|
-
key = path[curr_idx]
|
146
|
-
node = (node[key] || node[key] = curr_idx < last_idx ? {} : [])
|
147
|
-
curr_idx += 1
|
148
|
-
end
|
149
|
-
|
150
|
-
hash
|
151
|
-
end
|
136
|
+
def partition_message_values(values)
|
137
|
+
values
|
138
|
+
.map { |value| value.is_a?(Array) ? value : [value] }
|
139
|
+
.reduce(EMPTY_ARRAY.dup, :+)
|
140
|
+
.partition { |value| value.is_a?(Hash) && !value[:text].is_a?(String) }
|
152
141
|
end
|
153
142
|
end
|
154
143
|
end
|
data/lib/dry/schema/messages.rb
CHANGED
@@ -7,8 +7,8 @@ module Dry
|
|
7
7
|
# @api private
|
8
8
|
module Messages
|
9
9
|
BACKENDS = {
|
10
|
-
i18n:
|
11
|
-
yaml:
|
10
|
+
i18n: "I18n",
|
11
|
+
yaml: "YAML"
|
12
12
|
}.freeze
|
13
13
|
|
14
14
|
module_function
|
@@ -31,7 +31,7 @@ module Dry
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
require
|
35
|
-
require
|
36
|
-
require
|
37
|
-
require
|
34
|
+
require "dry/schema/messages/abstract"
|
35
|
+
require "dry/schema/messages/namespaced"
|
36
|
+
require "dry/schema/messages/yaml"
|
37
|
+
require "dry/schema/messages/i18n" if defined?(I18n)
|
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
3
|
+
require "set"
|
4
|
+
require "concurrent/map"
|
5
|
+
require "dry/equalizer"
|
6
|
+
require "dry/configurable"
|
7
7
|
|
8
|
-
require
|
9
|
-
require
|
8
|
+
require "dry/schema/constants"
|
9
|
+
require "dry/schema/messages/template"
|
10
10
|
|
11
11
|
module Dry
|
12
12
|
module Schema
|
@@ -21,36 +21,31 @@ module Dry
|
|
21
21
|
setting :default_locale, nil
|
22
22
|
setting :load_paths, Set[DEFAULT_MESSAGES_PATH]
|
23
23
|
setting :top_namespace, DEFAULT_MESSAGES_ROOT
|
24
|
-
setting :root,
|
24
|
+
setting :root, "errors"
|
25
25
|
setting :lookup_options, %i[root predicate path val_type arg_type].freeze
|
26
26
|
|
27
27
|
setting :lookup_paths, [
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
28
|
+
"%<root>s.rules.%<path>s.%<predicate>s.arg.%<arg_type>s",
|
29
|
+
"%<root>s.rules.%<path>s.%<predicate>s",
|
30
|
+
"%<root>s.%<predicate>s.%<message_type>s",
|
31
|
+
"%<root>s.%<predicate>s.value.%<path>s",
|
32
|
+
"%<root>s.%<predicate>s.value.%<val_type>s.arg.%<arg_type>s",
|
33
|
+
"%<root>s.%<predicate>s.value.%<val_type>s",
|
34
|
+
"%<root>s.%<predicate>s.arg.%<arg_type>s",
|
35
|
+
"%<root>s.%<predicate>s"
|
36
36
|
].freeze
|
37
37
|
|
38
|
-
setting :rule_lookup_paths, [
|
38
|
+
setting :rule_lookup_paths, ["rules.%<name>s"].freeze
|
39
39
|
|
40
|
-
setting :arg_types, Hash.new { |*|
|
41
|
-
Range =>
|
40
|
+
setting :arg_types, Hash.new { |*| "default" }.update(
|
41
|
+
Range => "range"
|
42
42
|
)
|
43
43
|
|
44
|
-
setting :val_types, Hash.new { |*|
|
45
|
-
Range =>
|
46
|
-
String =>
|
44
|
+
setting :val_types, Hash.new { |*| "default" }.update(
|
45
|
+
Range => "range",
|
46
|
+
String => "string"
|
47
47
|
)
|
48
48
|
|
49
|
-
# @api private
|
50
|
-
def self.cache
|
51
|
-
@cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
|
52
|
-
end
|
53
|
-
|
54
49
|
# @api private
|
55
50
|
def self.build(options = EMPTY_HASH)
|
56
51
|
messages = new
|
@@ -79,7 +74,7 @@ module Dry
|
|
79
74
|
|
80
75
|
# @api private
|
81
76
|
def rule(name, options = {})
|
82
|
-
tokens = {
|
77
|
+
tokens = {name: name, locale: options.fetch(:locale, default_locale)}
|
83
78
|
path = rule_lookup_paths(tokens).detect { |key| key?(key, options) }
|
84
79
|
|
85
80
|
rule = get(path, options) if path
|
@@ -92,13 +87,35 @@ module Dry
|
|
92
87
|
#
|
93
88
|
# @api public
|
94
89
|
def call(predicate, options)
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
90
|
+
options = {locale: default_locale, **options}
|
91
|
+
opts = options.reject { |k,| config.lookup_options.include?(k) }
|
92
|
+
path = lookup_paths(predicate, options).detect { |key| key?(key, opts) }
|
93
|
+
|
94
|
+
return unless path
|
95
|
+
|
96
|
+
result = get(path, opts)
|
97
|
+
|
98
|
+
[
|
99
|
+
Template.new(
|
100
|
+
messages: self,
|
101
|
+
key: path,
|
102
|
+
options: opts
|
103
|
+
),
|
104
|
+
result[:meta]
|
105
|
+
]
|
99
106
|
end
|
107
|
+
|
100
108
|
alias_method :[], :call
|
101
109
|
|
110
|
+
# Check if given key is defined
|
111
|
+
#
|
112
|
+
# @return [Boolean]
|
113
|
+
#
|
114
|
+
# @api public
|
115
|
+
def key?(_key, _options = EMPTY_HASH)
|
116
|
+
raise NotImplementedError
|
117
|
+
end
|
118
|
+
|
102
119
|
# Retrieve an array of looked up paths
|
103
120
|
#
|
104
121
|
# @param [Symbol] predicate
|
@@ -112,27 +129,6 @@ module Dry
|
|
112
129
|
filled_lookup_paths(tokens)
|
113
130
|
end
|
114
131
|
|
115
|
-
# Try to find a message for the given predicate and its options
|
116
|
-
#
|
117
|
-
# @api private
|
118
|
-
#
|
119
|
-
# rubocop:disable Metrics/AbcSize
|
120
|
-
def lookup(predicate, options)
|
121
|
-
opts = options.reject { |k, _| config.lookup_options.include?(k) }
|
122
|
-
path = lookup_paths(predicate, options).detect { |key| key?(key, opts) }
|
123
|
-
|
124
|
-
return unless path
|
125
|
-
|
126
|
-
text = get(path, opts)
|
127
|
-
|
128
|
-
if text.is_a?(Hash)
|
129
|
-
text.values_at(:text, :meta)
|
130
|
-
else
|
131
|
-
[text, EMPTY_HASH]
|
132
|
-
end
|
133
|
-
end
|
134
|
-
# rubocop:enable Metrics/AbcSize
|
135
|
-
|
136
132
|
# @api private
|
137
133
|
def lookup_paths(predicate, options)
|
138
134
|
tokens = lookup_tokens(predicate, options)
|
@@ -168,22 +164,18 @@ module Dry
|
|
168
164
|
end
|
169
165
|
|
170
166
|
# @api private
|
171
|
-
def
|
172
|
-
|
167
|
+
def default_locale
|
168
|
+
config.default_locale
|
173
169
|
end
|
174
170
|
|
175
171
|
# @api private
|
176
|
-
def
|
177
|
-
|
172
|
+
def interpolatable_data(_key, _options, **_data)
|
173
|
+
raise NotImplementedError
|
178
174
|
end
|
179
175
|
|
180
176
|
# @api private
|
181
|
-
def
|
182
|
-
|
183
|
-
[predicate, options.reject { |k,| k.equal?(:input) }]
|
184
|
-
else
|
185
|
-
[predicate, options]
|
186
|
-
end
|
177
|
+
def interpolate(_key, _options, **_data)
|
178
|
+
raise NotImplementedError
|
187
179
|
end
|
188
180
|
|
189
181
|
private
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "i18n"
|
4
|
+
require "dry/schema/messages/abstract"
|
5
5
|
|
6
6
|
module Dry
|
7
7
|
module Schema
|
@@ -29,7 +29,22 @@ module Dry
|
|
29
29
|
#
|
30
30
|
# @api public
|
31
31
|
def get(key, options = EMPTY_HASH)
|
32
|
-
|
32
|
+
return unless key
|
33
|
+
|
34
|
+
result = t.(key, locale: options.fetch(:locale, default_locale))
|
35
|
+
|
36
|
+
if result.is_a?(Hash)
|
37
|
+
text = result[:text]
|
38
|
+
meta = result.dup.tap { |h| h.delete(:text) }
|
39
|
+
else
|
40
|
+
text = result
|
41
|
+
meta = EMPTY_HASH.dup
|
42
|
+
end
|
43
|
+
|
44
|
+
{
|
45
|
+
text: text,
|
46
|
+
meta: meta
|
47
|
+
}
|
33
48
|
end
|
34
49
|
|
35
50
|
# Check if given key is defined
|
@@ -67,7 +82,7 @@ module Dry
|
|
67
82
|
top_namespace = config.top_namespace
|
68
83
|
|
69
84
|
mapped_data = data
|
70
|
-
.map { |k, v| [k, {
|
85
|
+
.map { |k, v| [k, {top_namespace => v[DEFAULT_MESSAGES_ROOT]}] }
|
71
86
|
.to_h
|
72
87
|
|
73
88
|
store_translations(mapped_data)
|
@@ -80,12 +95,23 @@ module Dry
|
|
80
95
|
end
|
81
96
|
|
82
97
|
# @api private
|
83
|
-
def
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
98
|
+
def interpolatable_data(_key, _options, **data)
|
99
|
+
data
|
100
|
+
end
|
101
|
+
|
102
|
+
# @api private
|
103
|
+
def interpolate(key, options, **data)
|
104
|
+
text_key = "#{key}.text"
|
105
|
+
|
106
|
+
opts = {
|
107
|
+
locale: default_locale,
|
108
|
+
**options,
|
109
|
+
**data
|
110
|
+
}
|
111
|
+
|
112
|
+
resolved_key = key?(text_key, opts) ? text_key : key
|
113
|
+
|
114
|
+
t.(resolved_key, **opts)
|
89
115
|
end
|
90
116
|
|
91
117
|
private
|
@@ -24,7 +24,7 @@ module Dry
|
|
24
24
|
@config = messages.config
|
25
25
|
@namespace = namespace
|
26
26
|
@messages = messages
|
27
|
-
@call_opts = {
|
27
|
+
@call_opts = {namespace: namespace}.freeze
|
28
28
|
end
|
29
29
|
|
30
30
|
# Get a message for the given key and its options
|
@@ -62,13 +62,23 @@ module Dry
|
|
62
62
|
# @api private
|
63
63
|
def rule_lookup_paths(tokens)
|
64
64
|
base_paths = messages.rule_lookup_paths(tokens)
|
65
|
-
base_paths.map { |key| key.gsub(
|
65
|
+
base_paths.map { |key| key.gsub("dry_schema", "dry_schema.#{namespace}") } + base_paths
|
66
66
|
end
|
67
67
|
|
68
68
|
# @api private
|
69
69
|
def cache_key(predicate, options)
|
70
70
|
messages.cache_key(predicate, options)
|
71
71
|
end
|
72
|
+
|
73
|
+
# @api private
|
74
|
+
def interpolatable_data(key, options, **data)
|
75
|
+
messages.interpolatable_data(key, options, **data)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @api private
|
79
|
+
def interpolate(key, options, **data)
|
80
|
+
messages.interpolate(key, options, **data)
|
81
|
+
end
|
72
82
|
end
|
73
83
|
end
|
74
84
|
end
|
@@ -1,67 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/initializer"
|
4
|
+
require "dry/equalizer"
|
4
5
|
|
5
|
-
require
|
6
|
+
require "dry/schema/constants"
|
6
7
|
|
7
8
|
module Dry
|
8
9
|
module Schema
|
9
10
|
module Messages
|
10
|
-
# Template wraps a string with interpolation tokens and defines evaluator function
|
11
|
-
# dynamically
|
12
|
-
#
|
13
11
|
# @api private
|
14
12
|
class Template
|
15
|
-
|
13
|
+
extend Dry::Initializer
|
14
|
+
include Dry::Equalizer(:messages, :key, :options)
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
# @return [String]
|
21
|
-
attr_reader :text
|
22
|
-
|
23
|
-
# !@attribute [r] tokens
|
24
|
-
# @return [Hash]
|
25
|
-
attr_reader :tokens
|
26
|
-
|
27
|
-
# !@attribute [r] evaluator
|
28
|
-
# @return [Proc]
|
29
|
-
attr_reader :evaluator
|
16
|
+
option :messages
|
17
|
+
option :key
|
18
|
+
option :options
|
30
19
|
|
31
20
|
# @api private
|
32
|
-
def
|
33
|
-
|
21
|
+
def data(data = EMPTY_HASH)
|
22
|
+
ensure_message!
|
23
|
+
messages.interpolatable_data(key, options, **options, **data)
|
34
24
|
end
|
35
25
|
|
36
26
|
# @api private
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
evaluator = <<-RUBY.strip
|
42
|
-
-> (#{tokens.map { |token| "#{token}:" }.join(", ")}) { "#{text}" }
|
43
|
-
RUBY
|
44
|
-
|
45
|
-
[text, tokens, eval(evaluator, binding, __FILE__, __LINE__ - 3)]
|
27
|
+
def call(data = EMPTY_HASH)
|
28
|
+
ensure_message!
|
29
|
+
messages.interpolate(key, options, **data)
|
46
30
|
end
|
31
|
+
alias_method :[], :call
|
47
32
|
|
48
|
-
|
49
|
-
def initialize(text, tokens, evaluator)
|
50
|
-
@text = text
|
51
|
-
@tokens = tokens
|
52
|
-
@evaluator = evaluator
|
53
|
-
end
|
33
|
+
private
|
54
34
|
|
55
|
-
|
56
|
-
|
57
|
-
tokens.each_with_object({}) { |k, h| h[k] = input[k] }
|
58
|
-
end
|
35
|
+
def ensure_message!
|
36
|
+
return if messages.key?(key, options)
|
59
37
|
|
60
|
-
|
61
|
-
def call(data = EMPTY_HASH)
|
62
|
-
data.empty? ? evaluator.() : evaluator.(data)
|
38
|
+
raise KeyError, "No message found for template, template=#{inspect}"
|
63
39
|
end
|
64
|
-
alias_method :[], :call
|
65
40
|
end
|
66
41
|
end
|
67
42
|
end
|