dry-schema 1.8.0 → 1.9.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 +40 -1
- data/README.md +4 -4
- data/dry-schema.gemspec +2 -2
- data/lib/dry/schema/compiler.rb +1 -1
- data/lib/dry/schema/dsl.rb +7 -4
- data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +9 -4
- data/lib/dry/schema/extensions/hints.rb +11 -9
- data/lib/dry/schema/extensions/info/schema_compiler.rb +10 -1
- data/lib/dry/schema/extensions/json_schema/schema_compiler.rb +232 -0
- data/lib/dry/schema/extensions/json_schema.rb +29 -0
- data/lib/dry/schema/extensions/struct.rb +1 -1
- data/lib/dry/schema/extensions.rb +4 -0
- data/lib/dry/schema/key.rb +75 -74
- data/lib/dry/schema/key_coercer.rb +2 -2
- data/lib/dry/schema/key_validator.rb +44 -23
- data/lib/dry/schema/macros/array.rb +4 -0
- data/lib/dry/schema/macros/core.rb +1 -1
- data/lib/dry/schema/macros/dsl.rb +17 -15
- data/lib/dry/schema/macros/hash.rb +1 -1
- data/lib/dry/schema/macros/key.rb +2 -2
- data/lib/dry/schema/macros/schema.rb +2 -0
- data/lib/dry/schema/macros/value.rb +7 -0
- data/lib/dry/schema/message/or/multi_path.rb +7 -5
- data/lib/dry/schema/message_compiler.rb +13 -10
- data/lib/dry/schema/messages/i18n.rb +98 -96
- data/lib/dry/schema/messages/namespaced.rb +6 -0
- data/lib/dry/schema/messages/yaml.rb +165 -158
- data/lib/dry/schema/predicate.rb +2 -2
- data/lib/dry/schema/predicate_inferrer.rb +2 -0
- data/lib/dry/schema/primitive_inferrer.rb +2 -0
- data/lib/dry/schema/processor.rb +2 -2
- data/lib/dry/schema/result.rb +5 -7
- data/lib/dry/schema/trace.rb +5 -1
- data/lib/dry/schema/type_registry.rb +1 -2
- data/lib/dry/schema/version.rb +1 -1
- metadata +11 -8
@@ -9,195 +9,202 @@ require "dry/schema/messages/abstract"
|
|
9
9
|
|
10
10
|
module Dry
|
11
11
|
module Schema
|
12
|
-
|
13
|
-
|
14
|
-
# @api public
|
15
|
-
class Messages::YAML < Messages::Abstract
|
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
|
23
|
-
|
24
|
-
include Dry::Equalizer(:data)
|
25
|
-
|
26
|
-
# Loaded localized message templates
|
12
|
+
module Messages
|
13
|
+
# Plain YAML message backend
|
27
14
|
#
|
28
|
-
# @
|
29
|
-
|
15
|
+
# @api public
|
16
|
+
class YAML < Abstract
|
17
|
+
LOCALE_TOKEN = "%<locale>s"
|
18
|
+
TOKEN_REGEXP = /%{(\w*)}/.freeze
|
19
|
+
EMPTY_CONTEXT = Object.new.tap { |ctx|
|
20
|
+
def ctx.context
|
21
|
+
binding
|
22
|
+
end
|
23
|
+
}.freeze.context
|
30
24
|
|
31
|
-
|
32
|
-
#
|
33
|
-
# @return [Proc]
|
34
|
-
attr_reader :t
|
25
|
+
include ::Dry::Equalizer(:data)
|
35
26
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
27
|
+
# Loaded localized message templates
|
28
|
+
#
|
29
|
+
# @return [Hash]
|
30
|
+
attr_reader :data
|
40
31
|
|
41
|
-
|
32
|
+
# Translation function
|
33
|
+
#
|
34
|
+
# @return [Proc]
|
35
|
+
attr_reader :t
|
42
36
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
37
|
+
# @api private
|
38
|
+
def self.build(options = EMPTY_HASH)
|
39
|
+
super do |config|
|
40
|
+
config.default_locale = :en unless config.default_locale
|
48
41
|
|
49
|
-
|
50
|
-
def self.flat_hash(hash, path = [], keys = {})
|
51
|
-
hash.each do |key, value|
|
52
|
-
flat_hash(value, [*path, key], keys) if value.is_a?(Hash)
|
42
|
+
config.root = -"%<locale>s.#{config.root}"
|
53
43
|
|
54
|
-
|
55
|
-
|
56
|
-
text: value,
|
57
|
-
meta: EMPTY_HASH
|
58
|
-
}
|
59
|
-
elsif value.is_a?(Hash) && value["text"].is_a?(String)
|
60
|
-
keys[[*path, key].join(DOT)] = {
|
61
|
-
text: value["text"],
|
62
|
-
meta: value.dup.delete_if { |k| k == "text" }.map { |k, v| [k.to_sym, v] }.to_h
|
44
|
+
config.rule_lookup_paths = config.rule_lookup_paths.map { |path|
|
45
|
+
-"%<locale>s.#{path}"
|
63
46
|
}
|
64
47
|
end
|
65
48
|
end
|
66
|
-
keys
|
67
|
-
end
|
68
|
-
|
69
|
-
# @api private
|
70
|
-
def self.cache
|
71
|
-
@cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
|
72
|
-
end
|
73
49
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
50
|
+
# @api private
|
51
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
52
|
+
def self.flat_hash(hash, path = EMPTY_ARRAY, keys = {})
|
53
|
+
hash.each do |key, value|
|
54
|
+
flat_hash(value, [*path, key], keys) if value.is_a?(Hash)
|
55
|
+
|
56
|
+
if value.is_a?(String) && hash["text"] != value
|
57
|
+
keys[[*path, key].join(DOT)] = {
|
58
|
+
text: value,
|
59
|
+
meta: EMPTY_HASH
|
60
|
+
}
|
61
|
+
elsif value.is_a?(Hash) && value["text"].is_a?(String)
|
62
|
+
keys[[*path, key].join(DOT)] = {
|
63
|
+
text: value["text"],
|
64
|
+
meta: value.reject { _1.eql?("text") }.transform_keys(&:to_sym)
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
86
68
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
# @param [Hash] options
|
91
|
-
#
|
92
|
-
# @return [String]
|
93
|
-
#
|
94
|
-
# @api public
|
95
|
-
def looked_up_paths(predicate, options)
|
96
|
-
super.map { |path| path % {locale: options[:locale] || default_locale} }
|
97
|
-
end
|
69
|
+
keys
|
70
|
+
end
|
71
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
98
72
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
#
|
104
|
-
# @return [String]
|
105
|
-
#
|
106
|
-
# @api public
|
107
|
-
def get(key, options = EMPTY_HASH)
|
108
|
-
data[evaluated_key(key, options)]
|
109
|
-
end
|
73
|
+
# @api private
|
74
|
+
def self.cache
|
75
|
+
@cache ||= Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new }
|
76
|
+
end
|
110
77
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
# @api public
|
116
|
-
def key?(key, options = EMPTY_HASH)
|
117
|
-
data.key?(evaluated_key(key, options))
|
118
|
-
end
|
78
|
+
# @api private
|
79
|
+
def self.source_cache
|
80
|
+
@source_cache ||= Concurrent::Map.new
|
81
|
+
end
|
119
82
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
# @api public
|
127
|
-
def merge(overrides)
|
128
|
-
if overrides.is_a?(Hash)
|
129
|
-
self.class.new(
|
130
|
-
data: data.merge(self.class.flat_hash(overrides)),
|
131
|
-
config: config
|
132
|
-
)
|
133
|
-
else
|
134
|
-
self.class.new(
|
135
|
-
data: Array(overrides).reduce(data) { |a, e| a.merge(load_translations(e)) },
|
136
|
-
config: config
|
137
|
-
)
|
83
|
+
# @api private
|
84
|
+
def initialize(data: EMPTY_HASH, config: nil)
|
85
|
+
super()
|
86
|
+
@data = data
|
87
|
+
@config = config if config
|
88
|
+
@t = proc { |key, locale: default_locale| get("%<locale>s.#{key}", locale: locale) }
|
138
89
|
end
|
139
|
-
end
|
140
90
|
|
141
|
-
|
142
|
-
|
143
|
-
@
|
144
|
-
|
145
|
-
|
91
|
+
# Get an array of looked up paths
|
92
|
+
#
|
93
|
+
# @param [Symbol] predicate
|
94
|
+
# @param [Hash] options
|
95
|
+
#
|
96
|
+
# @return [String]
|
97
|
+
#
|
98
|
+
# @api public
|
99
|
+
def looked_up_paths(predicate, options)
|
100
|
+
super.map { |path| path % {locale: options[:locale] || default_locale} }
|
101
|
+
end
|
146
102
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
103
|
+
# Get a message for the given key and its options
|
104
|
+
#
|
105
|
+
# @param [Symbol] key
|
106
|
+
# @param [Hash] options
|
107
|
+
#
|
108
|
+
# @return [String]
|
109
|
+
#
|
110
|
+
# @api public
|
111
|
+
def get(key, options = EMPTY_HASH)
|
112
|
+
data[evaluated_key(key, options)]
|
113
|
+
end
|
152
114
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
115
|
+
# Check if given key is defined
|
116
|
+
#
|
117
|
+
# @return [Boolean]
|
118
|
+
#
|
119
|
+
# @api public
|
120
|
+
def key?(key, options = EMPTY_HASH)
|
121
|
+
data.key?(evaluated_key(key, options))
|
122
|
+
end
|
158
123
|
|
159
|
-
|
124
|
+
# Merge messages from an additional path
|
125
|
+
#
|
126
|
+
# @param [String] overrides
|
127
|
+
#
|
128
|
+
# @return [Messages::I18n]
|
129
|
+
#
|
130
|
+
# @api public
|
131
|
+
def merge(overrides)
|
132
|
+
if overrides.is_a?(Hash)
|
133
|
+
self.class.new(
|
134
|
+
data: data.merge(self.class.flat_hash(overrides)),
|
135
|
+
config: config
|
136
|
+
)
|
137
|
+
else
|
138
|
+
self.class.new(
|
139
|
+
data: Array(overrides).reduce(data) { |a, e| a.merge(load_translations(e)) },
|
140
|
+
config: config
|
141
|
+
)
|
142
|
+
end
|
143
|
+
end
|
160
144
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
145
|
+
# @api private
|
146
|
+
def prepare
|
147
|
+
@data = config.load_paths.map { |path| load_translations(path) }.reduce({}, :merge)
|
148
|
+
self
|
149
|
+
end
|
166
150
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
151
|
+
# @api private
|
152
|
+
def interpolatable_data(key, options, **data)
|
153
|
+
tokens = evaluation_context(key, options).fetch(:tokens)
|
154
|
+
data.select { |k,| tokens.include?(k) }
|
155
|
+
end
|
172
156
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
157
|
+
# @api private
|
158
|
+
def interpolate(key, options, **data)
|
159
|
+
evaluator = evaluation_context(key, options).fetch(:evaluator)
|
160
|
+
data.empty? ? evaluator.() : evaluator.(**data)
|
177
161
|
end
|
178
|
-
end
|
179
162
|
|
180
|
-
|
181
|
-
|
182
|
-
@
|
183
|
-
|
163
|
+
private
|
164
|
+
|
165
|
+
# @api private
|
166
|
+
def evaluation_context(key, options)
|
167
|
+
cache.fetch_or_store(get(key, options).fetch(:text)) do |input|
|
168
|
+
tokens = input.scan(TOKEN_REGEXP).flatten(1).map(&:to_sym).to_set
|
169
|
+
text = input.gsub("%", "#")
|
170
|
+
|
171
|
+
# rubocop:disable Security/Eval
|
172
|
+
# rubocop:disable Style/DocumentDynamicEvalDefinition
|
173
|
+
evaluator = eval(<<~RUBY, EMPTY_CONTEXT, __FILE__, __LINE__ + 1)
|
174
|
+
-> (#{tokens.map { |token| "#{token}:" }.join(", ")}) { "#{text}" }
|
175
|
+
RUBY
|
176
|
+
# rubocop:enable Style/DocumentDynamicEvalDefinition
|
177
|
+
# rubocop:enable Security/Eval
|
178
|
+
|
179
|
+
{
|
180
|
+
tokens: tokens,
|
181
|
+
evaluator: evaluator
|
182
|
+
}
|
183
|
+
end
|
184
|
+
end
|
184
185
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
self.class.flat_hash(YAML.load_file(path)).freeze
|
186
|
+
# @api private
|
187
|
+
def cache
|
188
|
+
@cache ||= self.class.cache[self]
|
189
189
|
end
|
190
190
|
|
191
|
-
|
191
|
+
# @api private
|
192
|
+
def load_translations(path)
|
193
|
+
data = self.class.source_cache.fetch_or_store(path) do
|
194
|
+
self.class.flat_hash(::YAML.load_file(path)).freeze
|
195
|
+
end
|
192
196
|
|
193
|
-
|
194
|
-
end
|
197
|
+
return data unless custom_top_namespace?(path)
|
195
198
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
+
data.transform_keys { _1.gsub(DEFAULT_MESSAGES_ROOT, config.top_namespace) }
|
200
|
+
end
|
201
|
+
|
202
|
+
# @api private
|
203
|
+
def evaluated_key(key, options)
|
204
|
+
return key unless key.include?(LOCALE_TOKEN)
|
199
205
|
|
200
|
-
|
206
|
+
key % {locale: options[:locale] || default_locale}
|
207
|
+
end
|
201
208
|
end
|
202
209
|
end
|
203
210
|
end
|
data/lib/dry/schema/predicate.rb
CHANGED
data/lib/dry/schema/processor.rb
CHANGED
@@ -116,14 +116,14 @@ module Dry
|
|
116
116
|
->(input) { call(input) }
|
117
117
|
end
|
118
118
|
|
119
|
-
# Return string
|
119
|
+
# Return string representation
|
120
120
|
#
|
121
121
|
# @return [String]
|
122
122
|
#
|
123
123
|
# @api public
|
124
124
|
def inspect
|
125
125
|
<<~STR.strip
|
126
|
-
#<#{self.class.name} keys=#{key_map.map(&:dump)} rules=#{rules.
|
126
|
+
#<#{self.class.name} keys=#{key_map.map(&:dump)} rules=#{rules.transform_values(&:to_s)}>
|
127
127
|
STR
|
128
128
|
end
|
129
129
|
|
data/lib/dry/schema/result.rb
CHANGED
@@ -185,13 +185,11 @@ module Dry
|
|
185
185
|
"#<#{self.class}#{to_h.inspect} errors=#{errors.to_h.inspect} path=#{path.keys.inspect}>"
|
186
186
|
end
|
187
187
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
output
|
194
|
-
end
|
188
|
+
# Pattern matching support
|
189
|
+
#
|
190
|
+
# @api private
|
191
|
+
def deconstruct_keys(_)
|
192
|
+
output
|
195
193
|
end
|
196
194
|
|
197
195
|
# Add a new error AST node
|
data/lib/dry/schema/trace.rb
CHANGED
@@ -89,6 +89,10 @@ module Dry
|
|
89
89
|
captures.map(&:to_ast).map(&compiler.method(:visit)).reduce(:and)
|
90
90
|
end
|
91
91
|
|
92
|
+
def respond_to_missing?(meth, include_private = false)
|
93
|
+
super || (meth.to_s.end_with?(QUESTION_MARK) && compuiler.support?(meth))
|
94
|
+
end
|
95
|
+
|
92
96
|
# @api private
|
93
97
|
def method_missing(meth, *args, &block)
|
94
98
|
if meth.to_s.end_with?(QUESTION_MARK)
|
@@ -96,7 +100,7 @@ module Dry
|
|
96
100
|
::Kernel.raise InvalidSchemaError, "#{meth} predicate cannot be used in this context"
|
97
101
|
end
|
98
102
|
|
99
|
-
unless compiler.
|
103
|
+
unless compiler.support?(meth)
|
100
104
|
::Kernel.raise ::ArgumentError, "#{meth} predicate is not defined"
|
101
105
|
end
|
102
106
|
|
data/lib/dry/schema/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dry-schema
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.9.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Solnica
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -178,6 +178,8 @@ files:
|
|
178
178
|
- lib/dry/schema/extensions/hints/result_methods.rb
|
179
179
|
- lib/dry/schema/extensions/info.rb
|
180
180
|
- lib/dry/schema/extensions/info/schema_compiler.rb
|
181
|
+
- lib/dry/schema/extensions/json_schema.rb
|
182
|
+
- lib/dry/schema/extensions/json_schema/schema_compiler.rb
|
181
183
|
- lib/dry/schema/extensions/monads.rb
|
182
184
|
- lib/dry/schema/extensions/struct.rb
|
183
185
|
- lib/dry/schema/json.rb
|
@@ -235,10 +237,10 @@ licenses:
|
|
235
237
|
- MIT
|
236
238
|
metadata:
|
237
239
|
allowed_push_host: https://rubygems.org
|
238
|
-
changelog_uri: https://github.com/dry-rb/dry-schema/blob/
|
240
|
+
changelog_uri: https://github.com/dry-rb/dry-schema/blob/main/CHANGELOG.md
|
239
241
|
source_code_uri: https://github.com/dry-rb/dry-schema
|
240
242
|
bug_tracker_uri: https://github.com/dry-rb/dry-schema/issues
|
241
|
-
post_install_message:
|
243
|
+
post_install_message:
|
242
244
|
rdoc_options: []
|
243
245
|
require_paths:
|
244
246
|
- lib
|
@@ -246,15 +248,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
246
248
|
requirements:
|
247
249
|
- - ">="
|
248
250
|
- !ruby/object:Gem::Version
|
249
|
-
version: 2.
|
251
|
+
version: 2.7.0
|
250
252
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
251
253
|
requirements:
|
252
254
|
- - ">="
|
253
255
|
- !ruby/object:Gem::Version
|
254
256
|
version: '0'
|
255
257
|
requirements: []
|
256
|
-
rubygems_version: 3.
|
257
|
-
signing_key:
|
258
|
+
rubygems_version: 3.2.32
|
259
|
+
signing_key:
|
258
260
|
specification_version: 4
|
259
261
|
summary: Coercion and validation for data structures
|
260
262
|
test_files: []
|
263
|
+
...
|