dry-schema 1.8.0 → 1.9.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 +15 -0
- data/README.md +3 -3
- data/dry-schema.gemspec +1 -1
- 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 +1 -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 +1 -1
- 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 +5 -3
@@ -24,17 +24,7 @@ module Dry
|
|
24
24
|
key_paths = key_map.to_dot_notation
|
25
25
|
|
26
26
|
input_paths.each do |path|
|
27
|
-
error_path =
|
28
|
-
if path[INDEX_REGEX]
|
29
|
-
key = path.gsub(INDEX_REGEX, BRACKETS)
|
30
|
-
|
31
|
-
if key_paths.none? { |key_path| key_path.include?(key) }
|
32
|
-
arr = path.gsub(INDEX_REGEX) { |m| ".#{m[1]}" }
|
33
|
-
arr.split(DOT).map { |s| DIGIT_REGEX.match?(s) ? s.to_i : s.to_sym }
|
34
|
-
end
|
35
|
-
elsif key_paths.none? { |key_path| key_path.include?(path) }
|
36
|
-
path
|
37
|
-
end
|
27
|
+
error_path = validate_path(key_paths, path)
|
38
28
|
|
39
29
|
next unless error_path
|
40
30
|
|
@@ -47,26 +37,57 @@ module Dry
|
|
47
37
|
private
|
48
38
|
|
49
39
|
# @api private
|
50
|
-
def key_paths
|
51
|
-
|
52
|
-
|
53
|
-
when Hash
|
54
|
-
next key.to_s if value.empty?
|
40
|
+
def validate_path(key_paths, path)
|
41
|
+
if path[INDEX_REGEX]
|
42
|
+
key = path.gsub(INDEX_REGEX, BRACKETS)
|
55
43
|
|
56
|
-
|
57
|
-
|
58
|
-
|
44
|
+
if key_paths.none? { paths_match?(key, _1) }
|
45
|
+
arr = path.gsub(INDEX_REGEX) { ".#{_1[1]}" }
|
46
|
+
arr.split(DOT).map { DIGIT_REGEX.match?(_1) ? Integer(_1, 10) : _1.to_sym }
|
47
|
+
end
|
48
|
+
elsif key_paths.none? { paths_match?(path, _1) }
|
49
|
+
path
|
50
|
+
end
|
51
|
+
end
|
59
52
|
|
60
|
-
|
53
|
+
# @api private
|
54
|
+
def paths_match?(input_path, key_path)
|
55
|
+
residue = key_path.sub(input_path, "")
|
56
|
+
residue.empty? || residue.start_with?(DOT, BRACKETS)
|
57
|
+
end
|
61
58
|
|
62
|
-
|
63
|
-
|
64
|
-
|
59
|
+
# @api private
|
60
|
+
def key_paths(hash)
|
61
|
+
hash.flat_map { |key, value|
|
62
|
+
case value
|
63
|
+
when ::Hash
|
64
|
+
if value.empty?
|
65
|
+
[key.to_s]
|
66
|
+
else
|
67
|
+
[key].product(key_paths(hash[key])).map { _1.join(DOT) }
|
68
|
+
end
|
69
|
+
when ::Array
|
70
|
+
hashes_or_arrays = hashes_or_arrays(value)
|
71
|
+
|
72
|
+
if hashes_or_arrays.empty?
|
73
|
+
[key.to_s]
|
74
|
+
else
|
75
|
+
hashes_or_arrays.flat_map.with_index { |el, idx|
|
76
|
+
key_paths(el).map { ["#{key}[#{idx}]", *_1].join(DOT) }
|
77
|
+
}
|
78
|
+
end
|
65
79
|
else
|
66
80
|
key.to_s
|
67
81
|
end
|
68
82
|
}
|
69
83
|
end
|
84
|
+
|
85
|
+
# @api private
|
86
|
+
def hashes_or_arrays(xs)
|
87
|
+
xs.select { |x|
|
88
|
+
(x.is_a?(::Array) || x.is_a?(::Hash)) && !x.empty?
|
89
|
+
}
|
90
|
+
end
|
70
91
|
end
|
71
92
|
end
|
72
93
|
end
|
@@ -10,6 +10,8 @@ module Dry
|
|
10
10
|
# @api private
|
11
11
|
class Array < DSL
|
12
12
|
# @api private
|
13
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
14
|
+
# rubocop: disable Metrics/AbcSize
|
13
15
|
def value(*args, **opts, &block)
|
14
16
|
type(:array)
|
15
17
|
|
@@ -40,6 +42,8 @@ module Dry
|
|
40
42
|
|
41
43
|
self
|
42
44
|
end
|
45
|
+
# rubocop: enable Metrics/AbcSize
|
46
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
43
47
|
|
44
48
|
# @api private
|
45
49
|
def to_ast(*)
|
@@ -58,9 +58,9 @@ module Dry
|
|
58
58
|
# @return [Macros::Core]
|
59
59
|
#
|
60
60
|
# @api public
|
61
|
-
def value(
|
61
|
+
def value(...)
|
62
62
|
append_macro(Macros::Value) do |macro|
|
63
|
-
macro.call(
|
63
|
+
macro.call(...)
|
64
64
|
end
|
65
65
|
end
|
66
66
|
ruby2_keywords :value if respond_to?(:ruby2_keywords, true)
|
@@ -76,9 +76,9 @@ module Dry
|
|
76
76
|
# @return [Macros::Core]
|
77
77
|
#
|
78
78
|
# @api public
|
79
|
-
def filled(
|
79
|
+
def filled(...)
|
80
80
|
append_macro(Macros::Filled) do |macro|
|
81
|
-
macro.call(
|
81
|
+
macro.call(...)
|
82
82
|
end
|
83
83
|
end
|
84
84
|
ruby2_keywords :filled if respond_to?(:ruby2_keywords, true)
|
@@ -97,9 +97,9 @@ module Dry
|
|
97
97
|
# @return [Macros::Core]
|
98
98
|
#
|
99
99
|
# @api public
|
100
|
-
def schema(
|
100
|
+
def schema(...)
|
101
101
|
append_macro(Macros::Schema) do |macro|
|
102
|
-
macro.call(
|
102
|
+
macro.call(...)
|
103
103
|
end
|
104
104
|
end
|
105
105
|
ruby2_keywords :schema if respond_to?(:ruby2_keywords, true)
|
@@ -112,9 +112,9 @@ module Dry
|
|
112
112
|
# end
|
113
113
|
#
|
114
114
|
# @api public
|
115
|
-
def hash(
|
115
|
+
def hash(...)
|
116
116
|
append_macro(Macros::Hash) do |macro|
|
117
|
-
macro.call(
|
117
|
+
macro.call(...)
|
118
118
|
end
|
119
119
|
end
|
120
120
|
ruby2_keywords :hash if respond_to?(:ruby2_keywords, true)
|
@@ -136,9 +136,9 @@ module Dry
|
|
136
136
|
# @return [Macros::Core]
|
137
137
|
#
|
138
138
|
# @api public
|
139
|
-
def each(
|
139
|
+
def each(...)
|
140
140
|
append_macro(Macros::Each) do |macro|
|
141
|
-
macro.value(
|
141
|
+
macro.value(...)
|
142
142
|
end
|
143
143
|
end
|
144
144
|
ruby2_keywords :each if respond_to?(:ruby2_keywords, true)
|
@@ -156,9 +156,9 @@ module Dry
|
|
156
156
|
# @return [Macros::Core]
|
157
157
|
#
|
158
158
|
# @api public
|
159
|
-
def array(
|
159
|
+
def array(...)
|
160
160
|
append_macro(Macros::Array) do |macro|
|
161
|
-
macro.value(
|
161
|
+
macro.value(...)
|
162
162
|
end
|
163
163
|
end
|
164
164
|
ruby2_keywords :array if respond_to?(:ruby2_keywords, true)
|
@@ -200,10 +200,11 @@ module Dry
|
|
200
200
|
end
|
201
201
|
|
202
202
|
# @api private
|
203
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
203
204
|
def extract_type_spec(*args, nullable: false, set_type: true)
|
204
205
|
type_spec = args[0] unless schema_or_predicate?(args[0])
|
205
206
|
|
206
|
-
predicates = Array(type_spec ? args[1
|
207
|
+
predicates = Array(type_spec ? args[1..] : args)
|
207
208
|
type_rule = nil
|
208
209
|
|
209
210
|
if type_spec
|
@@ -228,6 +229,7 @@ module Dry
|
|
228
229
|
yield(*predicates, type_spec: type_spec, type_rule: nil)
|
229
230
|
end
|
230
231
|
end
|
232
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
231
233
|
|
232
234
|
# @api private
|
233
235
|
def resolve_type(type_spec, nullable)
|
@@ -243,8 +245,8 @@ module Dry
|
|
243
245
|
# @api private
|
244
246
|
def schema_or_predicate?(arg)
|
245
247
|
arg.is_a?(Dry::Schema::Processor) ||
|
246
|
-
arg.is_a?(Symbol) &&
|
247
|
-
arg.to_s.end_with?(QUESTION_MARK)
|
248
|
+
(arg.is_a?(Symbol) &&
|
249
|
+
arg.to_s.end_with?(QUESTION_MARK))
|
248
250
|
end
|
249
251
|
end
|
250
252
|
end
|
@@ -26,8 +26,8 @@ module Dry
|
|
26
26
|
# @return [Macros::Key]
|
27
27
|
#
|
28
28
|
# @api public
|
29
|
-
def filter(
|
30
|
-
(filter_schema_dsl[name] || filter_schema_dsl.optional(name)).value(
|
29
|
+
def filter(...)
|
30
|
+
(filter_schema_dsl[name] || filter_schema_dsl.optional(name)).value(...)
|
31
31
|
self
|
32
32
|
end
|
33
33
|
ruby2_keywords(:filter) if respond_to?(:ruby2_keywords, true)
|
@@ -45,6 +45,7 @@ module Dry
|
|
45
45
|
end
|
46
46
|
|
47
47
|
# @api private
|
48
|
+
# rubocop: disable Metrics/AbcSize
|
48
49
|
def define(*args, &block)
|
49
50
|
definition = schema_dsl.new(path: schema_dsl.path, &block)
|
50
51
|
schema = definition.call
|
@@ -66,6 +67,7 @@ module Dry
|
|
66
67
|
|
67
68
|
schema
|
68
69
|
end
|
70
|
+
# rubocop: enable Metrics/AbcSize
|
69
71
|
|
70
72
|
# @api private
|
71
73
|
def parent_type
|
@@ -11,6 +11,10 @@ module Dry
|
|
11
11
|
# @api private
|
12
12
|
class Value < DSL
|
13
13
|
# @api private
|
14
|
+
#
|
15
|
+
# rubocop:disable Metrics/AbcSize
|
16
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
17
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
14
18
|
def call(*args, **opts, &block)
|
15
19
|
types, predicates = args.partition { |arg| arg.is_a?(Dry::Types::Type) }
|
16
20
|
|
@@ -65,6 +69,9 @@ module Dry
|
|
65
69
|
|
66
70
|
self
|
67
71
|
end
|
72
|
+
# rubocop:enable Metrics/AbcSize
|
73
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
74
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
68
75
|
|
69
76
|
# @api private
|
70
77
|
def array_type?(type)
|
@@ -17,17 +17,19 @@ module Dry
|
|
17
17
|
attr_reader :root
|
18
18
|
|
19
19
|
# @api private
|
20
|
-
def initialize(
|
20
|
+
def initialize(...)
|
21
21
|
super
|
22
|
-
|
23
|
-
|
24
|
-
@
|
22
|
+
flat_left = left.flatten
|
23
|
+
flat_right = right.flatten
|
24
|
+
@root = [*flat_left, *flat_right].map(&:_path).reduce(:&)
|
25
|
+
@left = flat_left.map { _1.to_or(root) }
|
26
|
+
@right = flat_right.map { _1.to_or(root) }
|
25
27
|
end
|
26
28
|
|
27
29
|
# @api public
|
28
30
|
def to_h
|
29
31
|
@to_h ||= Path[[*root, :or]].to_h(
|
30
|
-
[
|
32
|
+
[MessageSet.new(left).to_h, MessageSet.new(right).to_h]
|
31
33
|
)
|
32
34
|
end
|
33
35
|
end
|
@@ -30,14 +30,14 @@ module Dry
|
|
30
30
|
|
31
31
|
EMPTY_OPTS = VisitorOpts.new
|
32
32
|
EMPTY_MESSAGE_SET = MessageSet.new(EMPTY_ARRAY).freeze
|
33
|
-
FULL_MESSAGE_WHITESPACE = Hash.new(
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
33
|
+
FULL_MESSAGE_WHITESPACE = Hash.new(" ").merge(
|
34
|
+
ja: "",
|
35
|
+
zh: "",
|
36
|
+
bn: "",
|
37
|
+
th: "",
|
38
|
+
lo: "",
|
39
|
+
my: ""
|
40
|
+
)
|
41
41
|
|
42
42
|
param :messages
|
43
43
|
|
@@ -206,7 +206,8 @@ module Dry
|
|
206
206
|
return text if !text || !full
|
207
207
|
|
208
208
|
rule = options[:path]
|
209
|
-
[messages.rule(rule, options) || rule,
|
209
|
+
[messages.rule(rule, options) || rule,
|
210
|
+
text].join(FULL_MESSAGE_WHITESPACE[template.options[:locale]])
|
210
211
|
end
|
211
212
|
|
212
213
|
# @api private
|
@@ -228,7 +229,9 @@ module Dry
|
|
228
229
|
# @api private
|
229
230
|
def append_mapped_size_tokens(tokens)
|
230
231
|
# this is a temporary fix for the inconsistency in the "size" errors arguments
|
231
|
-
mapped_hash = tokens.each_with_object({}) { |(k, v), h|
|
232
|
+
mapped_hash = tokens.each_with_object({}) { |(k, v), h|
|
233
|
+
h[k.to_s.gsub("size", "num").to_sym] = v
|
234
|
+
}
|
232
235
|
tokens.merge(mapped_hash)
|
233
236
|
end
|
234
237
|
end
|
@@ -5,125 +5,127 @@ require "dry/schema/messages/abstract"
|
|
5
5
|
|
6
6
|
module Dry
|
7
7
|
module Schema
|
8
|
-
|
9
|
-
|
10
|
-
# @api public
|
11
|
-
class Messages::I18n < Messages::Abstract
|
12
|
-
# Translation function
|
13
|
-
#
|
14
|
-
# @return [Method]
|
15
|
-
attr_reader :t
|
16
|
-
|
17
|
-
# @api private
|
18
|
-
def initialize
|
19
|
-
super
|
20
|
-
@t = I18n.method(:t)
|
21
|
-
end
|
22
|
-
|
23
|
-
# Get a message for the given key and its options
|
24
|
-
#
|
25
|
-
# @param [Symbol] key
|
26
|
-
# @param [Hash] options
|
27
|
-
#
|
28
|
-
# @return [String]
|
8
|
+
module Messages
|
9
|
+
# I18n message backend
|
29
10
|
#
|
30
11
|
# @api public
|
31
|
-
|
32
|
-
|
12
|
+
class I18n < Abstract
|
13
|
+
# Translation function
|
14
|
+
#
|
15
|
+
# @return [Method]
|
16
|
+
attr_reader :t
|
17
|
+
|
18
|
+
# @api private
|
19
|
+
def initialize
|
20
|
+
super
|
21
|
+
@t = ::I18n.method(:t)
|
22
|
+
end
|
33
23
|
|
34
|
-
|
24
|
+
# Get a message for the given key and its options
|
25
|
+
#
|
26
|
+
# @param [Symbol] key
|
27
|
+
# @param [Hash] options
|
28
|
+
#
|
29
|
+
# @return [String]
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def get(key, options = EMPTY_HASH)
|
33
|
+
return unless key
|
34
|
+
|
35
|
+
result = t.(key, locale: options.fetch(:locale, default_locale))
|
36
|
+
|
37
|
+
if result.is_a?(Hash)
|
38
|
+
text = result[:text]
|
39
|
+
meta = result.dup.tap { |h| h.delete(:text) }
|
40
|
+
else
|
41
|
+
text = result
|
42
|
+
meta = EMPTY_HASH.dup
|
43
|
+
end
|
35
44
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
text = result
|
41
|
-
meta = EMPTY_HASH.dup
|
45
|
+
{
|
46
|
+
text: text,
|
47
|
+
meta: meta
|
48
|
+
}
|
42
49
|
end
|
43
50
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
#
|
54
|
-
# @api public
|
55
|
-
def key?(key, options)
|
56
|
-
I18n.exists?(key, options.fetch(:locale, default_locale)) ||
|
57
|
-
I18n.exists?(key, I18n.default_locale)
|
58
|
-
end
|
51
|
+
# Check if given key is defined
|
52
|
+
#
|
53
|
+
# @return [Boolean]
|
54
|
+
#
|
55
|
+
# @api public
|
56
|
+
def key?(key, options)
|
57
|
+
::I18n.exists?(key, options.fetch(:locale, default_locale)) ||
|
58
|
+
::I18n.exists?(key, ::I18n.default_locale)
|
59
|
+
end
|
59
60
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
61
|
+
# Merge messages from an additional path
|
62
|
+
#
|
63
|
+
# @param [String, Array<String>] paths
|
64
|
+
#
|
65
|
+
# @return [Messages::I18n]
|
66
|
+
#
|
67
|
+
# @api public
|
68
|
+
def merge(paths)
|
69
|
+
prepare(paths)
|
70
|
+
end
|
70
71
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
72
|
+
# @api private
|
73
|
+
def default_locale
|
74
|
+
super || ::I18n.locale || ::I18n.default_locale
|
75
|
+
end
|
75
76
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
77
|
+
# @api private
|
78
|
+
def prepare(paths = config.load_paths)
|
79
|
+
paths.each do |path|
|
80
|
+
data = ::YAML.load_file(path)
|
80
81
|
|
81
|
-
|
82
|
-
|
82
|
+
if custom_top_namespace?(path)
|
83
|
+
top_namespace = config.top_namespace
|
83
84
|
|
84
|
-
|
85
|
-
|
86
|
-
|
85
|
+
mapped_data = data.transform_values { |v|
|
86
|
+
{top_namespace => v[DEFAULT_MESSAGES_ROOT]}
|
87
|
+
}
|
87
88
|
|
88
|
-
|
89
|
-
|
90
|
-
|
89
|
+
store_translations(mapped_data)
|
90
|
+
else
|
91
|
+
store_translations(data)
|
92
|
+
end
|
91
93
|
end
|
92
|
-
end
|
93
94
|
|
94
|
-
|
95
|
-
|
95
|
+
self
|
96
|
+
end
|
96
97
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
98
|
+
# @api private
|
99
|
+
def interpolatable_data(_key, _options, **data)
|
100
|
+
data
|
101
|
+
end
|
101
102
|
|
102
|
-
|
103
|
-
|
104
|
-
|
103
|
+
# @api private
|
104
|
+
def interpolate(key, options, **data)
|
105
|
+
text_key = "#{key}.text"
|
105
106
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
107
|
+
opts = {
|
108
|
+
locale: default_locale,
|
109
|
+
**options,
|
110
|
+
**data
|
111
|
+
}
|
111
112
|
|
112
|
-
|
113
|
+
resolved_key = key?(text_key, opts) ? text_key : key
|
113
114
|
|
114
|
-
|
115
|
-
|
115
|
+
t.(resolved_key, **opts)
|
116
|
+
end
|
116
117
|
|
117
|
-
|
118
|
+
private
|
118
119
|
|
119
|
-
|
120
|
-
|
121
|
-
|
120
|
+
# @api private
|
121
|
+
def store_translations(data)
|
122
|
+
locales = data.keys.map(&:to_sym)
|
122
123
|
|
123
|
-
|
124
|
+
::I18n.available_locales |= locales
|
124
125
|
|
125
|
-
|
126
|
-
|
126
|
+
locales.each do |locale|
|
127
|
+
::I18n.backend.store_translations(locale, data[locale.to_s])
|
128
|
+
end
|
127
129
|
end
|
128
130
|
end
|
129
131
|
end
|