dry-schema 1.4.3 → 1.5.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 +170 -97
- data/config/errors.yml +4 -0
- data/dry-schema.gemspec +46 -0
- data/lib/dry-schema.rb +1 -1
- data/lib/dry/schema.rb +19 -6
- data/lib/dry/schema/compiler.rb +4 -4
- data/lib/dry/schema/config.rb +15 -6
- data/lib/dry/schema/constants.rb +16 -7
- data/lib/dry/schema/dsl.rb +88 -27
- 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 +1 -1
- 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 +16 -1
- 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 +66 -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 +9 -4
- data/lib/dry/schema/macros/dsl.rb +34 -19
- 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 +9 -9
- data/lib/dry/schema/macros/maybe.rb +3 -3
- 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 +32 -10
- 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 +37 -17
- data/lib/dry/schema/message_compiler/visitor_opts.rb +2 -2
- data/lib/dry/schema/message_set.rb +25 -36
- data/lib/dry/schema/messages.rb +6 -6
- data/lib/dry/schema/messages/abstract.rb +54 -56
- data/lib/dry/schema/messages/i18n.rb +29 -27
- 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 +60 -13
- data/lib/dry/schema/params.rb +1 -1
- data/lib/dry/schema/path.rb +44 -5
- data/lib/dry/schema/predicate.rb +2 -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 +49 -28
- data/lib/dry/schema/processor_steps.rb +50 -27
- data/lib/dry/schema/result.rb +43 -5
- data/lib/dry/schema/rule_applier.rb +8 -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 +22 -8
@@ -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
|
@@ -31,21 +31,19 @@ module Dry
|
|
31
31
|
def get(key, options = EMPTY_HASH)
|
32
32
|
return unless key
|
33
33
|
|
34
|
-
|
34
|
+
result = t.(key, locale: options.fetch(:locale, default_locale))
|
35
35
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
meta: EMPTY_HASH
|
43
|
-
}
|
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
|
44
42
|
end
|
45
43
|
|
46
44
|
{
|
47
|
-
text:
|
48
|
-
meta:
|
45
|
+
text: text,
|
46
|
+
meta: meta
|
49
47
|
}
|
50
48
|
end
|
51
49
|
|
@@ -84,7 +82,7 @@ module Dry
|
|
84
82
|
top_namespace = config.top_namespace
|
85
83
|
|
86
84
|
mapped_data = data
|
87
|
-
.map { |k, v| [k, {
|
85
|
+
.map { |k, v| [k, {top_namespace => v[DEFAULT_MESSAGES_ROOT]}] }
|
88
86
|
.to_h
|
89
87
|
|
90
88
|
store_translations(mapped_data)
|
@@ -97,12 +95,23 @@ module Dry
|
|
97
95
|
end
|
98
96
|
|
99
97
|
# @api private
|
100
|
-
def
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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)
|
106
115
|
end
|
107
116
|
|
108
117
|
private
|
@@ -117,13 +126,6 @@ module Dry
|
|
117
126
|
I18n.backend.store_translations(locale, data[locale.to_s])
|
118
127
|
end
|
119
128
|
end
|
120
|
-
|
121
|
-
def extract_meta(parent_key, translation, options)
|
122
|
-
translation.keys.each_with_object({}) do |k, meta|
|
123
|
-
meta_key = "#{parent_key}.#{k}"
|
124
|
-
meta[k] = t.(meta_key, **options) if k != :text && key?(meta_key, options)
|
125
|
-
end
|
126
|
-
end
|
127
129
|
end
|
128
130
|
end
|
129
131
|
end
|
@@ -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
|
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "yaml"
|
4
|
+
require "pathname"
|
5
5
|
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
6
|
+
require "dry/equalizer"
|
7
|
+
require "dry/schema/constants"
|
8
|
+
require "dry/schema/messages/abstract"
|
9
9
|
|
10
10
|
module Dry
|
11
11
|
module Schema
|
@@ -13,7 +13,13 @@ module Dry
|
|
13
13
|
#
|
14
14
|
# @api public
|
15
15
|
class Messages::YAML < Messages::Abstract
|
16
|
-
LOCALE_TOKEN =
|
16
|
+
LOCALE_TOKEN = "%<locale>s"
|
17
|
+
TOKEN_REGEXP = /%{(\w*)}/.freeze
|
18
|
+
EMPTY_CONTEXT = Object.new.tap { |ctx|
|
19
|
+
def ctx.context
|
20
|
+
binding
|
21
|
+
end
|
22
|
+
}.freeze.context
|
17
23
|
|
18
24
|
include Dry::Equalizer(:data)
|
19
25
|
|
@@ -45,21 +51,26 @@ module Dry
|
|
45
51
|
hash.each do |key, value|
|
46
52
|
flat_hash(value, [*path, key], keys) if value.is_a?(Hash)
|
47
53
|
|
48
|
-
if value.is_a?(String) && hash[
|
54
|
+
if value.is_a?(String) && hash["text"] != value
|
49
55
|
keys[[*path, key].join(DOT)] = {
|
50
56
|
text: value,
|
51
57
|
meta: EMPTY_HASH
|
52
58
|
}
|
53
|
-
elsif value.is_a?(Hash) && value[
|
59
|
+
elsif value.is_a?(Hash) && value["text"].is_a?(String)
|
54
60
|
keys[[*path, key].join(DOT)] = {
|
55
|
-
text: value[
|
56
|
-
meta: value.dup.delete_if { |k| k ==
|
61
|
+
text: value["text"],
|
62
|
+
meta: value.dup.delete_if { |k| k == "text" }.map { |k, v| [k.to_sym, v] }.to_h
|
57
63
|
}
|
58
64
|
end
|
59
65
|
end
|
60
66
|
keys
|
61
67
|
end
|
62
68
|
|
69
|
+
# @api private
|
70
|
+
def self.cache
|
71
|
+
@cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
|
72
|
+
end
|
73
|
+
|
63
74
|
# @api private
|
64
75
|
def initialize(data: EMPTY_HASH, config: nil)
|
65
76
|
super()
|
@@ -77,7 +88,7 @@ module Dry
|
|
77
88
|
#
|
78
89
|
# @api public
|
79
90
|
def looked_up_paths(predicate, options)
|
80
|
-
super.map { |path| path % {
|
91
|
+
super.map { |path| path % {locale: options[:locale] || default_locale} }
|
81
92
|
end
|
82
93
|
|
83
94
|
# Get a message for the given key and its options
|
@@ -124,12 +135,48 @@ module Dry
|
|
124
135
|
|
125
136
|
# @api private
|
126
137
|
def prepare
|
127
|
-
@data = config.load_paths.map { |path| load_translations(path) }.reduce(:merge)
|
138
|
+
@data = config.load_paths.map { |path| load_translations(path) }.reduce({}, :merge)
|
128
139
|
self
|
129
140
|
end
|
130
141
|
|
142
|
+
# @api private
|
143
|
+
def interpolatable_data(key, options, **data)
|
144
|
+
tokens = evaluation_context(key, options).fetch(:tokens)
|
145
|
+
data.select { |k,| tokens.include?(k) }
|
146
|
+
end
|
147
|
+
|
148
|
+
# @api private
|
149
|
+
def interpolate(key, options, **data)
|
150
|
+
evaluator = evaluation_context(key, options).fetch(:evaluator)
|
151
|
+
data.empty? ? evaluator.() : evaluator.(**data)
|
152
|
+
end
|
153
|
+
|
131
154
|
private
|
132
155
|
|
156
|
+
# @api private
|
157
|
+
def evaluation_context(key, options)
|
158
|
+
cache.fetch_or_store(get(key, options).fetch(:text)) do |input|
|
159
|
+
tokens = input.scan(TOKEN_REGEXP).flatten(1).map(&:to_sym).to_set
|
160
|
+
text = input.gsub("%", "#")
|
161
|
+
|
162
|
+
# rubocop:disable Security/Eval
|
163
|
+
evaluator = eval(<<~RUBY, EMPTY_CONTEXT, __FILE__, __LINE__ + 1)
|
164
|
+
-> (#{tokens.map { |token| "#{token}:" }.join(", ")}) { "#{text}" }
|
165
|
+
RUBY
|
166
|
+
# rubocop:enable Security/Eval
|
167
|
+
|
168
|
+
{
|
169
|
+
tokens: tokens,
|
170
|
+
evaluator: evaluator
|
171
|
+
}
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# @api private
|
176
|
+
def cache
|
177
|
+
@cache ||= self.class.cache[self]
|
178
|
+
end
|
179
|
+
|
133
180
|
# @api private
|
134
181
|
def load_translations(path)
|
135
182
|
data = self.class.flat_hash(YAML.load_file(path))
|
@@ -143,7 +190,7 @@ module Dry
|
|
143
190
|
def evaluated_key(key, options)
|
144
191
|
return key unless key.include?(LOCALE_TOKEN)
|
145
192
|
|
146
|
-
key % {
|
193
|
+
key % {locale: options[:locale] || default_locale}
|
147
194
|
end
|
148
195
|
end
|
149
196
|
end
|
data/lib/dry/schema/params.rb
CHANGED
data/lib/dry/schema/path.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/schema/constants"
|
4
4
|
|
5
5
|
module Dry
|
6
6
|
module Schema
|
@@ -8,6 +8,7 @@ module Dry
|
|
8
8
|
#
|
9
9
|
# @api private
|
10
10
|
class Path
|
11
|
+
include Dry.Equalizer(:keys)
|
11
12
|
include Comparable
|
12
13
|
include Enumerable
|
13
14
|
|
@@ -23,7 +24,7 @@ module Dry
|
|
23
24
|
# @return [Path]
|
24
25
|
#
|
25
26
|
# @api private
|
26
|
-
def self.
|
27
|
+
def self.call(spec)
|
27
28
|
case spec
|
28
29
|
when Symbol, Array
|
29
30
|
new(Array[*spec])
|
@@ -34,10 +35,15 @@ module Dry
|
|
34
35
|
when Path
|
35
36
|
spec
|
36
37
|
else
|
37
|
-
raise ArgumentError,
|
38
|
+
raise ArgumentError, "+spec+ must be either a Symbol, Array, Hash or a Path"
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
42
|
+
# @api private
|
43
|
+
def self.[](spec)
|
44
|
+
call(spec)
|
45
|
+
end
|
46
|
+
|
41
47
|
# Extract a list of keys from a hash
|
42
48
|
#
|
43
49
|
# @api private
|
@@ -52,6 +58,28 @@ module Dry
|
|
52
58
|
@keys = keys
|
53
59
|
end
|
54
60
|
|
61
|
+
# @api private
|
62
|
+
def to_h(value = EMPTY_ARRAY.dup)
|
63
|
+
curr_idx = 0
|
64
|
+
last_idx = keys.size - 1
|
65
|
+
hash = EMPTY_HASH.dup
|
66
|
+
node = hash
|
67
|
+
|
68
|
+
while curr_idx <= last_idx
|
69
|
+
node =
|
70
|
+
node[keys[curr_idx]] =
|
71
|
+
if curr_idx == last_idx
|
72
|
+
value.is_a?(Array) ? value : [value]
|
73
|
+
else
|
74
|
+
EMPTY_HASH.dup
|
75
|
+
end
|
76
|
+
|
77
|
+
curr_idx += 1
|
78
|
+
end
|
79
|
+
|
80
|
+
hash
|
81
|
+
end
|
82
|
+
|
55
83
|
# @api private
|
56
84
|
def each(&block)
|
57
85
|
keys.each(&block)
|
@@ -83,8 +111,19 @@ module Dry
|
|
83
111
|
end
|
84
112
|
|
85
113
|
# @api private
|
86
|
-
def
|
87
|
-
|
114
|
+
def &(other)
|
115
|
+
unless same_root?(other)
|
116
|
+
raise ArgumentError, "#{other.inspect} doesn't have the same root #{inspect}"
|
117
|
+
end
|
118
|
+
|
119
|
+
self.class.new(
|
120
|
+
key_matches(other, :select).compact.reject { |value| value.equal?(false) }
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
# @api private
|
125
|
+
def key_matches(other, meth = :map)
|
126
|
+
public_send(meth) { |key| (idx = other.index(key)) && keys[idx].equal?(key) }
|
88
127
|
end
|
89
128
|
|
90
129
|
# @api private
|