dry-schema 1.14.1 → 1.15.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 +196 -208
- data/LICENSE +1 -1
- data/README.md +8 -13
- data/config/errors.yml +6 -0
- data/dry-schema.gemspec +26 -22
- data/lib/dry/schema/config.rb +1 -1
- data/lib/dry/schema/extensions/json_schema/schema_compiler.rb +74 -11
- data/lib/dry/schema/extensions/struct.rb +14 -2
- data/lib/dry/schema/key_validator.rb +19 -6
- data/lib/dry/schema/macros/dsl.rb +4 -0
- data/lib/dry/schema/message.rb +8 -1
- data/lib/dry/schema/message_compiler.rb +3 -3
- data/lib/dry/schema/messages/abstract.rb +0 -1
- data/lib/dry/schema/messages/i18n.rb +6 -2
- data/lib/dry/schema/predicate_inferrer.rb +10 -1
- data/lib/dry/schema/primitive_inferrer.rb +7 -1
- data/lib/dry/schema/trace.rb +4 -2
- data/lib/dry/schema/version.rb +1 -1
- metadata +85 -31
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
<!---
|
|
2
|
-
|
|
1
|
+
<!--- This file is synced from hanakai-rb/repo-sync -->
|
|
2
|
+
|
|
3
|
+
[rubygem]: https://rubygems.org/gems/dry-schema
|
|
3
4
|
[actions]: https://github.com/dry-rb/dry-schema/actions
|
|
4
5
|
|
|
5
|
-
# dry-schema [][
|
|
6
|
+
# dry-schema [][rubygem] [][actions]
|
|
6
7
|
|
|
7
8
|
## Links
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Supported Ruby versions
|
|
14
|
-
|
|
15
|
-
This library officially supports the following Ruby versions:
|
|
16
|
-
|
|
17
|
-
* MRI `>= 3.1`
|
|
18
|
-
* jruby `>= 9.4` (not tested on CI)
|
|
10
|
+
- [User documentation](https://dry-rb.org/gems/dry-schema)
|
|
11
|
+
- [API documentation](http://rubydoc.info/gems/dry-schema)
|
|
12
|
+
- [Forum](https://discourse.dry-rb.org)
|
|
19
13
|
|
|
20
14
|
## License
|
|
21
15
|
|
|
22
16
|
See `LICENSE` file.
|
|
17
|
+
|
data/config/errors.yml
CHANGED
data/dry-schema.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# This file is synced from hanakai-rb/repo-sync. To update it, edit repo-sync.yml.
|
|
4
4
|
|
|
5
5
|
lib = File.expand_path("lib", __dir__)
|
|
6
6
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
@@ -8,8 +8,8 @@ require "dry/schema/version"
|
|
|
8
8
|
|
|
9
9
|
Gem::Specification.new do |spec|
|
|
10
10
|
spec.name = "dry-schema"
|
|
11
|
-
spec.authors = ["
|
|
12
|
-
spec.email = ["
|
|
11
|
+
spec.authors = ["Hanakai team"]
|
|
12
|
+
spec.email = ["info@hanakai.org"]
|
|
13
13
|
spec.license = "MIT"
|
|
14
14
|
spec.version = Dry::Schema::VERSION.dup
|
|
15
15
|
|
|
@@ -18,29 +18,33 @@ Gem::Specification.new do |spec|
|
|
|
18
18
|
dry-schema provides a DSL for defining schemas with keys and rules that should be applied to
|
|
19
19
|
values. It supports coercion, input sanitization, custom types and localized error messages
|
|
20
20
|
(with or without I18n gem). It's also used as the schema engine in dry-validation.
|
|
21
|
-
|
|
22
21
|
TEXT
|
|
23
22
|
spec.homepage = "https://dry-rb.org/gems/dry-schema"
|
|
24
|
-
spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-schema.gemspec",
|
|
25
|
-
"lib/**/*", "config/*.yml"]
|
|
23
|
+
spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-schema.gemspec", "lib/**/*", "config/*.yml"]
|
|
26
24
|
spec.bindir = "bin"
|
|
27
25
|
spec.executables = []
|
|
28
26
|
spec.require_paths = ["lib"]
|
|
29
27
|
|
|
30
|
-
spec.
|
|
31
|
-
|
|
32
|
-
spec.metadata["
|
|
33
|
-
spec.metadata["
|
|
34
|
-
spec.metadata["
|
|
35
|
-
|
|
36
|
-
spec.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
spec.
|
|
41
|
-
spec.
|
|
42
|
-
spec.
|
|
43
|
-
spec.
|
|
44
|
-
spec.
|
|
45
|
-
spec.
|
|
28
|
+
spec.extra_rdoc_files = ["README.md", "CHANGELOG.md", "LICENSE"]
|
|
29
|
+
|
|
30
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
31
|
+
spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-schema/blob/main/CHANGELOG.md"
|
|
32
|
+
spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-schema"
|
|
33
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-schema/issues"
|
|
34
|
+
spec.metadata["funding_uri"] = "https://github.com/sponsors/hanami"
|
|
35
|
+
|
|
36
|
+
spec.required_ruby_version = ">= 3.2"
|
|
37
|
+
|
|
38
|
+
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
|
|
39
|
+
spec.add_runtime_dependency "zeitwerk", "~> 2.6"
|
|
40
|
+
spec.add_runtime_dependency "dry-core", "~> 1.1"
|
|
41
|
+
spec.add_runtime_dependency "dry-configurable", "~> 1.0", ">= 1.0.1"
|
|
42
|
+
spec.add_runtime_dependency "dry-initializer", "~> 3.2"
|
|
43
|
+
spec.add_runtime_dependency "dry-logic", "~> 1.6"
|
|
44
|
+
spec.add_runtime_dependency "dry-types", "~> 1.8"
|
|
45
|
+
spec.add_development_dependency "bundler"
|
|
46
|
+
spec.add_development_dependency "rake"
|
|
47
|
+
spec.add_development_dependency "rspec"
|
|
48
|
+
spec.add_development_dependency "yard"
|
|
46
49
|
end
|
|
50
|
+
|
data/lib/dry/schema/config.rb
CHANGED
|
@@ -46,7 +46,7 @@ module Dry
|
|
|
46
46
|
setting :backend, default: :yaml
|
|
47
47
|
setting :namespace
|
|
48
48
|
setting :load_paths, default: ::Set[DEFAULT_MESSAGES_PATH], constructor: :dup.to_proc
|
|
49
|
-
setting :top_namespace, default: DEFAULT_MESSAGES_ROOT
|
|
49
|
+
setting :top_namespace, default: DEFAULT_MESSAGES_ROOT, constructor: :to_s.to_proc
|
|
50
50
|
setting :default_locale
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -28,6 +28,14 @@ module Dry
|
|
|
28
28
|
time?: {type: "string", format: "time"},
|
|
29
29
|
min_size?: {minLength: TO_INTEGER},
|
|
30
30
|
max_size?: {maxLength: TO_INTEGER},
|
|
31
|
+
size?: {maxLength: TO_INTEGER, minLength: TO_INTEGER},
|
|
32
|
+
format?: {
|
|
33
|
+
pattern: proc do |x|
|
|
34
|
+
x.to_s.delete_prefix("(?-mix:").delete_suffix(")")
|
|
35
|
+
end
|
|
36
|
+
},
|
|
37
|
+
true?: {},
|
|
38
|
+
false?: {},
|
|
31
39
|
included_in?: {enum: ->(v, _) { v.to_a }},
|
|
32
40
|
filled?: EMPTY_HASH,
|
|
33
41
|
uri?: {format: "uri"},
|
|
@@ -46,6 +54,15 @@ module Dry
|
|
|
46
54
|
uuid_v5?: {
|
|
47
55
|
pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
|
|
48
56
|
},
|
|
57
|
+
uuid_v6?: {
|
|
58
|
+
pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-6[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
|
|
59
|
+
},
|
|
60
|
+
uuid_v7?: {
|
|
61
|
+
pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-7[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
|
|
62
|
+
},
|
|
63
|
+
uuid_v8?: {
|
|
64
|
+
pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-8[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
|
|
65
|
+
},
|
|
49
66
|
gt?: {exclusiveMinimum: IDENTITY},
|
|
50
67
|
gteq?: {minimum: IDENTITY},
|
|
51
68
|
lt?: {exclusiveMaximum: IDENTITY},
|
|
@@ -163,18 +180,64 @@ module Dry
|
|
|
163
180
|
name, rest = node
|
|
164
181
|
|
|
165
182
|
if name.equal?(:key?)
|
|
166
|
-
|
|
167
|
-
keys[prop_name] = {}
|
|
183
|
+
handle_key_predicate(rest)
|
|
168
184
|
else
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
handle_value_predicate(name, rest, opts)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @api private
|
|
190
|
+
def handle_key_predicate(rest)
|
|
191
|
+
prop_name = rest[0][1]
|
|
192
|
+
keys[prop_name] = {}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# @api private
|
|
196
|
+
def handle_value_predicate(name, rest, opts)
|
|
197
|
+
target = keys[opts[:key]]
|
|
198
|
+
type_opts = fetch_type_opts_for_predicate(name, rest, target)
|
|
199
|
+
|
|
200
|
+
if array_with_size_predicate?(target, name, opts)
|
|
201
|
+
apply_array_size_constraint(target, name, rest)
|
|
202
|
+
elsif target[:type]&.include?("array")
|
|
203
|
+
apply_array_item_constraint(target, type_opts)
|
|
204
|
+
else
|
|
205
|
+
merge_opts!(target, type_opts)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# @api private
|
|
210
|
+
def array_with_size_predicate?(target, name, opts)
|
|
211
|
+
target[:type]&.include?("array") && array_size_predicate?(name) && !opts[:member]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# @api private
|
|
215
|
+
def apply_array_size_constraint(target, name, rest)
|
|
216
|
+
array_type_opts = convert_array_size_predicate(name, rest)
|
|
217
|
+
merge_opts!(target, array_type_opts)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# @api private
|
|
221
|
+
def apply_array_item_constraint(target, type_opts)
|
|
222
|
+
target[:items] ||= {}
|
|
223
|
+
merge_opts!(target[:items], type_opts)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# @api private
|
|
227
|
+
def array_size_predicate?(name)
|
|
228
|
+
name == :min_size? || name == :max_size?
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# @api private
|
|
232
|
+
def convert_array_size_predicate(name, rest)
|
|
233
|
+
value = rest[0][1].to_i
|
|
234
|
+
case name
|
|
235
|
+
when :min_size?
|
|
236
|
+
{minItems: value}
|
|
237
|
+
when :max_size?
|
|
238
|
+
{maxItems: value}
|
|
239
|
+
else
|
|
240
|
+
{}
|
|
178
241
|
end
|
|
179
242
|
end
|
|
180
243
|
|
|
@@ -27,7 +27,8 @@ module Dry
|
|
|
27
27
|
"a struct class (#{name.inspect} => #{args[0]})"
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
struct_class = extract_struct_class(args[0])
|
|
31
|
+
schema = struct_compiler.(struct_class)
|
|
31
32
|
|
|
32
33
|
super(schema, *args.drop(1))
|
|
33
34
|
type(schema_dsl.types[name].constructor(schema))
|
|
@@ -39,7 +40,18 @@ module Dry
|
|
|
39
40
|
private
|
|
40
41
|
|
|
41
42
|
def struct?(type)
|
|
42
|
-
type.is_a?(::Class) && type <= ::Dry::Struct
|
|
43
|
+
(type.is_a?(::Class) && type <= ::Dry::Struct) ||
|
|
44
|
+
(type.is_a?(::Dry::Types::Constructor) && type.primitive <= ::Dry::Struct)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def extract_struct_class(type)
|
|
48
|
+
if type.is_a?(::Class) && type <= ::Dry::Struct
|
|
49
|
+
type
|
|
50
|
+
elsif type.is_a?(::Dry::Types::Constructor) && type.primitive <= ::Dry::Struct
|
|
51
|
+
type.primitive
|
|
52
|
+
else
|
|
53
|
+
type
|
|
54
|
+
end
|
|
43
55
|
end
|
|
44
56
|
})
|
|
45
57
|
end
|
|
@@ -9,6 +9,7 @@ module Dry
|
|
|
9
9
|
class KeyValidator
|
|
10
10
|
extend ::Dry::Initializer
|
|
11
11
|
|
|
12
|
+
MULTI_INDEX_REGEX = /(\[\d+\])+/
|
|
12
13
|
INDEX_REGEX = /\[\d+\]/
|
|
13
14
|
DIGIT_REGEX = /\A\d+\z/
|
|
14
15
|
BRACKETS = "[]"
|
|
@@ -38,10 +39,10 @@ module Dry
|
|
|
38
39
|
|
|
39
40
|
# @api private
|
|
40
41
|
def validate_path(key_paths, path)
|
|
41
|
-
if path[
|
|
42
|
-
key = path.gsub(
|
|
42
|
+
if path[MULTI_INDEX_REGEX]
|
|
43
|
+
key = path.gsub(MULTI_INDEX_REGEX, BRACKETS)
|
|
43
44
|
if none_key_paths_match?(key_paths, key)
|
|
44
|
-
arr = path.gsub(INDEX_REGEX) { ".#{_1[1]}" }
|
|
45
|
+
arr = path.gsub(INDEX_REGEX) { ".#{_1[1...-1]}" }
|
|
45
46
|
arr.split(DOT).map { DIGIT_REGEX.match?(_1) ? Integer(_1, 10) : _1.to_sym }
|
|
46
47
|
end
|
|
47
48
|
elsif none_key_paths_match?(key_paths, path)
|
|
@@ -83,9 +84,7 @@ module Dry
|
|
|
83
84
|
if hashes_or_arrays.empty?
|
|
84
85
|
[key.to_s]
|
|
85
86
|
else
|
|
86
|
-
|
|
87
|
-
key_paths(el).map { ["#{key}[#{idx}]", *_1].join(DOT) }
|
|
88
|
-
}
|
|
87
|
+
traverse_array(value).map { |sub_path| [key, sub_path].join("") }
|
|
89
88
|
end
|
|
90
89
|
else
|
|
91
90
|
key.to_s
|
|
@@ -99,6 +98,20 @@ module Dry
|
|
|
99
98
|
(x.is_a?(::Array) || x.is_a?(::Hash)) && !x.empty?
|
|
100
99
|
}
|
|
101
100
|
end
|
|
101
|
+
|
|
102
|
+
# @api private
|
|
103
|
+
def traverse_array(arr)
|
|
104
|
+
arr.each_with_index.flat_map do |el, idx|
|
|
105
|
+
case el
|
|
106
|
+
when ::Hash
|
|
107
|
+
key_paths(el).map { ["[#{idx}]", *_1].join(DOT) }
|
|
108
|
+
when ::Array
|
|
109
|
+
traverse_array(el).map { ["[#{idx}]", *_1].join("") }
|
|
110
|
+
else
|
|
111
|
+
[]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
102
115
|
end
|
|
103
116
|
end
|
|
104
117
|
end
|
|
@@ -234,6 +234,10 @@ module Dry
|
|
|
234
234
|
type_rule = [type_spec.left, type_spec.right].map { |ts|
|
|
235
235
|
new(klass: Core, chain: false).value(ts)
|
|
236
236
|
}.reduce(:|)
|
|
237
|
+
elsif type_spec.is_a?(Dry::Types::Intersection) && set_type
|
|
238
|
+
type_rule = [type_spec.left, type_spec.right].map { |ts|
|
|
239
|
+
new(klass: Core, chain: false).value(ts)
|
|
240
|
+
}.reduce(:&)
|
|
237
241
|
else
|
|
238
242
|
type_predicates = predicate_inferrer[resolved_type]
|
|
239
243
|
|
data/lib/dry/schema/message.rb
CHANGED
|
@@ -81,7 +81,7 @@ module Dry
|
|
|
81
81
|
# @api private
|
|
82
82
|
def to_or(root)
|
|
83
83
|
clone = dup
|
|
84
|
-
clone.instance_variable_set("@path", path
|
|
84
|
+
clone.instance_variable_set("@path", remove_prefix(path, prefix: root.to_a))
|
|
85
85
|
clone.instance_variable_set("@_path", nil)
|
|
86
86
|
clone
|
|
87
87
|
end
|
|
@@ -104,6 +104,13 @@ module Dry
|
|
|
104
104
|
def _path
|
|
105
105
|
@_path ||= Path[path]
|
|
106
106
|
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
def remove_prefix(array, prefix:)
|
|
111
|
+
has_prefix = array[0, prefix.length] == prefix
|
|
112
|
+
has_prefix ? array[prefix.length..] : array.dup
|
|
113
|
+
end
|
|
107
114
|
end
|
|
108
115
|
end
|
|
109
116
|
end
|
|
@@ -224,9 +224,9 @@ module Dry
|
|
|
224
224
|
# @api private
|
|
225
225
|
def append_mapped_size_tokens(tokens)
|
|
226
226
|
# this is a temporary fix for the inconsistency in the "size" errors arguments
|
|
227
|
-
mapped_hash = tokens.
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
mapped_hash = tokens.transform_keys do |k|
|
|
228
|
+
k.to_s.gsub("size", "num").to_sym
|
|
229
|
+
end
|
|
230
230
|
tokens.merge(mapped_hash)
|
|
231
231
|
end
|
|
232
232
|
end
|
|
@@ -32,7 +32,8 @@ module Dry
|
|
|
32
32
|
def get(key, options = EMPTY_HASH)
|
|
33
33
|
return unless key
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
options[:locale] ||= default_locale
|
|
36
|
+
result = t.(key, **options)
|
|
36
37
|
|
|
37
38
|
if result.is_a?(Hash)
|
|
38
39
|
text = result[:text]
|
|
@@ -112,7 +113,10 @@ module Dry
|
|
|
112
113
|
|
|
113
114
|
resolved_key = key?(text_key, opts) ? text_key : key
|
|
114
115
|
|
|
115
|
-
t.(resolved_key, **opts)
|
|
116
|
+
result = t.(resolved_key, **opts)
|
|
117
|
+
return result unless result.is_a?(Hash)
|
|
118
|
+
|
|
119
|
+
result[:text]
|
|
116
120
|
end
|
|
117
121
|
|
|
118
122
|
private
|
|
@@ -4,7 +4,16 @@ module Dry
|
|
|
4
4
|
module Schema
|
|
5
5
|
# @api private
|
|
6
6
|
class PredicateInferrer < ::Dry::Types::PredicateInferrer
|
|
7
|
-
Compiler = ::Class.new(superclass::Compiler)
|
|
7
|
+
Compiler = ::Class.new(superclass::Compiler) do
|
|
8
|
+
# @api private
|
|
9
|
+
def visit_intersection(node)
|
|
10
|
+
left_node, right_node, = node
|
|
11
|
+
left = visit(left_node)
|
|
12
|
+
right = visit(right_node)
|
|
13
|
+
|
|
14
|
+
[left, right].flatten.compact
|
|
15
|
+
end
|
|
16
|
+
end
|
|
8
17
|
|
|
9
18
|
def initialize(registry = PredicateRegistry.new)
|
|
10
19
|
super
|
|
@@ -4,7 +4,13 @@ module Dry
|
|
|
4
4
|
module Schema
|
|
5
5
|
# @api private
|
|
6
6
|
class PrimitiveInferrer < ::Dry::Types::PrimitiveInferrer
|
|
7
|
-
Compiler = ::Class.new(superclass::Compiler)
|
|
7
|
+
Compiler = ::Class.new(superclass::Compiler) do
|
|
8
|
+
# @api private
|
|
9
|
+
def visit_intersection(node)
|
|
10
|
+
left, right = node
|
|
11
|
+
[visit(left), visit(right)].flatten(1)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
8
14
|
|
|
9
15
|
def initialize
|
|
10
16
|
super
|
data/lib/dry/schema/trace.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Dry
|
|
|
7
7
|
# @api private
|
|
8
8
|
class Trace < ::BasicObject
|
|
9
9
|
INVALID_PREDICATES = %i[key?].freeze
|
|
10
|
+
RESPOND_TO_MISSING_METHOD = ::Kernel.instance_method(:respond_to_missing?)
|
|
10
11
|
|
|
11
12
|
include ::Dry::Equalizer(:compiler, :captures)
|
|
12
13
|
|
|
@@ -86,12 +87,13 @@ module Dry
|
|
|
86
87
|
end
|
|
87
88
|
|
|
88
89
|
def respond_to_missing?(meth, include_private = false)
|
|
89
|
-
|
|
90
|
+
RESPOND_TO_MISSING_METHOD.bind_call(self, meth, include_private) ||
|
|
91
|
+
(meth.to_s.end_with?(QUESTION_MARK) && compiler.support?(meth))
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
# @api private
|
|
93
95
|
def method_missing(meth, *args, &block)
|
|
94
|
-
if meth.to_s.end_with?(QUESTION_MARK)
|
|
96
|
+
if !meth.equal?(:respond_to_missing?) && meth.to_s.end_with?(QUESTION_MARK)
|
|
95
97
|
if ::Dry::Schema::Trace::INVALID_PREDICATES.include?(meth)
|
|
96
98
|
::Kernel.raise InvalidSchemaError, "#{meth} predicate cannot be used in this context"
|
|
97
99
|
end
|