dry-types 0.15.0 → 1.2.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/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +18 -2
- data/.travis.yml +10 -5
- data/.yardopts +6 -2
- data/CHANGELOG.md +186 -3
- data/Gemfile +11 -5
- data/README.md +4 -3
- data/Rakefile +4 -2
- data/benchmarks/hash_schemas.rb +10 -6
- data/benchmarks/lax_schema.rb +15 -0
- data/benchmarks/profile_invalid_input.rb +15 -0
- data/benchmarks/profile_lax_schema_valid.rb +16 -0
- data/benchmarks/profile_valid_input.rb +15 -0
- data/benchmarks/schema_valid_vs_invalid.rb +21 -0
- data/benchmarks/setup.rb +17 -0
- data/docsite/source/array-with-member.html.md +13 -0
- data/docsite/source/built-in-types.html.md +116 -0
- data/docsite/source/constraints.html.md +31 -0
- data/docsite/source/custom-types.html.md +93 -0
- data/docsite/source/default-values.html.md +91 -0
- data/docsite/source/enum.html.md +69 -0
- data/docsite/source/getting-started.html.md +57 -0
- data/docsite/source/hash-schemas.html.md +169 -0
- data/docsite/source/index.html.md +155 -0
- data/docsite/source/map.html.md +17 -0
- data/docsite/source/optional-values.html.md +96 -0
- data/docsite/source/sum.html.md +21 -0
- data/dry-types.gemspec +21 -19
- data/lib/dry-types.rb +2 -0
- data/lib/dry/types.rb +60 -17
- data/lib/dry/types/any.rb +21 -10
- data/lib/dry/types/array.rb +17 -1
- data/lib/dry/types/array/constructor.rb +32 -0
- data/lib/dry/types/array/member.rb +72 -13
- data/lib/dry/types/builder.rb +49 -5
- data/lib/dry/types/builder_methods.rb +43 -16
- data/lib/dry/types/coercions.rb +84 -19
- data/lib/dry/types/coercions/json.rb +22 -3
- data/lib/dry/types/coercions/params.rb +98 -30
- data/lib/dry/types/compiler.rb +35 -12
- data/lib/dry/types/constrained.rb +78 -27
- data/lib/dry/types/constrained/coercible.rb +36 -6
- data/lib/dry/types/constraints.rb +15 -1
- data/lib/dry/types/constructor.rb +77 -62
- data/lib/dry/types/constructor/function.rb +200 -0
- data/lib/dry/types/container.rb +5 -0
- data/lib/dry/types/core.rb +35 -14
- data/lib/dry/types/decorator.rb +37 -10
- data/lib/dry/types/default.rb +48 -16
- data/lib/dry/types/enum.rb +31 -16
- data/lib/dry/types/errors.rb +73 -7
- data/lib/dry/types/extensions.rb +6 -0
- data/lib/dry/types/extensions/maybe.rb +52 -5
- data/lib/dry/types/extensions/monads.rb +29 -0
- data/lib/dry/types/fn_container.rb +5 -0
- data/lib/dry/types/hash.rb +32 -14
- data/lib/dry/types/hash/constructor.rb +16 -3
- data/lib/dry/types/inflector.rb +2 -0
- data/lib/dry/types/json.rb +7 -5
- data/lib/dry/types/{safe.rb → lax.rb} +33 -16
- data/lib/dry/types/map.rb +70 -32
- data/lib/dry/types/meta.rb +51 -0
- data/lib/dry/types/module.rb +10 -5
- data/lib/dry/types/nominal.rb +105 -14
- data/lib/dry/types/options.rb +12 -25
- data/lib/dry/types/params.rb +14 -3
- data/lib/dry/types/predicate_inferrer.rb +197 -0
- data/lib/dry/types/predicate_registry.rb +34 -0
- data/lib/dry/types/primitive_inferrer.rb +97 -0
- data/lib/dry/types/printable.rb +5 -1
- data/lib/dry/types/printer.rb +70 -64
- data/lib/dry/types/result.rb +26 -0
- data/lib/dry/types/schema.rb +177 -80
- data/lib/dry/types/schema/key.rb +48 -35
- data/lib/dry/types/spec/types.rb +43 -6
- data/lib/dry/types/sum.rb +70 -21
- data/lib/dry/types/type.rb +49 -0
- data/lib/dry/types/version.rb +3 -1
- metadata +91 -62
data/lib/dry/types/result.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dry/equalizer'
|
2
4
|
|
3
5
|
module Dry
|
4
6
|
module Types
|
7
|
+
# Result class used by {Type#try}
|
8
|
+
#
|
9
|
+
# @api public
|
5
10
|
class Result
|
6
11
|
include Dry::Equalizer(:input, inspect: false)
|
7
12
|
|
@@ -9,22 +14,34 @@ module Dry
|
|
9
14
|
attr_reader :input
|
10
15
|
|
11
16
|
# @param [Object] input
|
17
|
+
#
|
18
|
+
# @api private
|
12
19
|
def initialize(input)
|
13
20
|
@input = input
|
14
21
|
end
|
15
22
|
|
23
|
+
# Success result
|
24
|
+
#
|
25
|
+
# @api public
|
16
26
|
class Success < Result
|
17
27
|
# @return [true]
|
28
|
+
#
|
29
|
+
# @api public
|
18
30
|
def success?
|
19
31
|
true
|
20
32
|
end
|
21
33
|
|
22
34
|
# @return [false]
|
35
|
+
#
|
36
|
+
# @api public
|
23
37
|
def failure?
|
24
38
|
false
|
25
39
|
end
|
26
40
|
end
|
27
41
|
|
42
|
+
# Failure result
|
43
|
+
#
|
44
|
+
# @api public
|
28
45
|
class Failure < Result
|
29
46
|
include Dry::Equalizer(:input, :error, inspect: false)
|
30
47
|
|
@@ -32,23 +49,32 @@ module Dry
|
|
32
49
|
attr_reader :error
|
33
50
|
|
34
51
|
# @param [Object] input
|
52
|
+
#
|
35
53
|
# @param [#to_s] error
|
54
|
+
#
|
55
|
+
# @api private
|
36
56
|
def initialize(input, error)
|
37
57
|
super(input)
|
38
58
|
@error = error
|
39
59
|
end
|
40
60
|
|
41
61
|
# @return [String]
|
62
|
+
#
|
63
|
+
# @api private
|
42
64
|
def to_s
|
43
65
|
error.to_s
|
44
66
|
end
|
45
67
|
|
46
68
|
# @return [false]
|
69
|
+
#
|
70
|
+
# @api public
|
47
71
|
def success?
|
48
72
|
false
|
49
73
|
end
|
50
74
|
|
51
75
|
# @return [true]
|
76
|
+
#
|
77
|
+
# @api public
|
52
78
|
def failure?
|
53
79
|
true
|
54
80
|
end
|
data/lib/dry/types/schema.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dry/types/fn_container'
|
2
4
|
|
3
5
|
module Dry
|
@@ -14,6 +16,8 @@ module Dry
|
|
14
16
|
# @see Dry::Types::Default::Callable#evaluate
|
15
17
|
#
|
16
18
|
# {Schema} implements Enumerable using its keys as collection.
|
19
|
+
#
|
20
|
+
# @api public
|
17
21
|
class Schema < Hash
|
18
22
|
NO_TRANSFORM = Dry::Types::FnContainer.register { |x| x }
|
19
23
|
SYMBOLIZE_KEY = Dry::Types::FnContainer.register(:to_sym.to_proc)
|
@@ -31,8 +35,11 @@ module Dry
|
|
31
35
|
|
32
36
|
# @param [Class] _primitive
|
33
37
|
# @param [Hash] options
|
38
|
+
#
|
34
39
|
# @option options [Array[Dry::Types::Schema::Key]] :keys
|
35
40
|
# @option options [String] :key_transform_fn
|
41
|
+
#
|
42
|
+
# @api private
|
36
43
|
def initialize(_primitive, **options)
|
37
44
|
@keys = options.fetch(:keys)
|
38
45
|
@name_key_map = keys.each_with_object({}) do |key, idx|
|
@@ -47,58 +54,96 @@ module Dry
|
|
47
54
|
end
|
48
55
|
|
49
56
|
# @param [Hash] hash
|
57
|
+
#
|
58
|
+
# @return [Hash{Symbol => Object}]
|
59
|
+
#
|
60
|
+
# @api private
|
61
|
+
def call_unsafe(hash, options = EMPTY_HASH)
|
62
|
+
resolve_unsafe(coerce(hash), options)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param [Hash] hash
|
66
|
+
#
|
50
67
|
# @return [Hash{Symbol => Object}]
|
51
|
-
|
52
|
-
|
68
|
+
#
|
69
|
+
# @api private
|
70
|
+
def call_safe(hash, options = EMPTY_HASH)
|
71
|
+
resolve_safe(coerce(hash) { return yield }, options) { return yield }
|
53
72
|
end
|
54
|
-
alias_method :[], :call
|
55
73
|
|
56
74
|
# @param [Hash] hash
|
75
|
+
#
|
57
76
|
# @option options [Boolean] :skip_missing If true don't raise error if on missing keys
|
58
77
|
# @option options [Boolean] :resolve_defaults If false default value
|
59
78
|
# won't be evaluated for missing key
|
60
79
|
# @return [Hash{Symbol => Object}]
|
80
|
+
#
|
81
|
+
# @api public
|
61
82
|
def apply(hash, options = EMPTY_HASH)
|
62
|
-
|
83
|
+
call_unsafe(hash, options)
|
63
84
|
end
|
64
85
|
|
65
86
|
# @param [Hash] hash
|
87
|
+
#
|
66
88
|
# @yieldparam [Failure] failure
|
67
89
|
# @yieldreturn [Result]
|
90
|
+
#
|
68
91
|
# @return [Logic::Result]
|
69
92
|
# @return [Object] if coercion fails and a block is given
|
70
|
-
|
71
|
-
|
93
|
+
#
|
94
|
+
# @api public
|
95
|
+
def try(input)
|
96
|
+
if primitive?(input)
|
72
97
|
success = true
|
73
|
-
output
|
98
|
+
output = {}
|
99
|
+
result = {}
|
100
|
+
|
101
|
+
input.each do |key, value|
|
102
|
+
k = @transform_key.(key)
|
103
|
+
type = @name_key_map[k]
|
74
104
|
|
75
|
-
|
76
|
-
|
105
|
+
if type
|
106
|
+
key_result = type.try(value)
|
107
|
+
result[k] = key_result
|
108
|
+
output[k] = key_result.input
|
77
109
|
success &&= key_result.success?
|
78
|
-
|
110
|
+
elsif strict?
|
111
|
+
success = false
|
112
|
+
end
|
113
|
+
end
|
79
114
|
|
80
|
-
|
115
|
+
if output.size < keys.size
|
116
|
+
resolve_missing_keys(output, options) do
|
117
|
+
success = false
|
81
118
|
end
|
82
|
-
|
83
|
-
|
84
|
-
|
119
|
+
end
|
120
|
+
|
121
|
+
success &&= primitive?(output)
|
122
|
+
|
123
|
+
if success
|
124
|
+
failure = nil
|
125
|
+
else
|
126
|
+
error = CoercionError.new("#{input} doesn't conform schema", meta: result)
|
127
|
+
failure = failure(output, error)
|
85
128
|
end
|
86
129
|
else
|
87
|
-
|
88
|
-
output = hash
|
89
|
-
result = "#{hash} must be a hash"
|
130
|
+
failure = failure(input, CoercionError.new("#{input} must be a hash"))
|
90
131
|
end
|
91
132
|
|
92
|
-
if
|
133
|
+
if failure.nil?
|
93
134
|
success(output)
|
135
|
+
elsif block_given?
|
136
|
+
yield(failure)
|
94
137
|
else
|
95
|
-
failure
|
96
|
-
block_given? ? yield(failure) : failure
|
138
|
+
failure
|
97
139
|
end
|
98
140
|
end
|
99
141
|
|
100
142
|
# @param meta [Boolean] Whether to dump the meta to the AST
|
143
|
+
#
|
101
144
|
# @return [Array] An AST representation
|
145
|
+
#
|
146
|
+
# @api public
|
102
147
|
def to_ast(meta: true)
|
103
148
|
if RUBY_VERSION >= "2.5"
|
104
149
|
opts = options.slice(:key_transform_fn, :type_transform_fn, :strict)
|
@@ -110,53 +155,53 @@ module Dry
|
|
110
155
|
|
111
156
|
[
|
112
157
|
:schema,
|
113
|
-
[
|
114
|
-
|
115
|
-
|
116
|
-
meta ? self.meta : EMPTY_HASH
|
117
|
-
]
|
158
|
+
[keys.map { |key| key.to_ast(meta: meta) },
|
159
|
+
opts,
|
160
|
+
meta ? self.meta : EMPTY_HASH]
|
118
161
|
]
|
119
162
|
end
|
120
163
|
|
121
|
-
# @param [Hash] hash
|
122
|
-
# @return [Boolean]
|
123
|
-
def valid?(hash)
|
124
|
-
result = try(hash)
|
125
|
-
result.success?
|
126
|
-
end
|
127
|
-
alias_method :===, :valid?
|
128
|
-
|
129
164
|
# Whether the schema rejects unknown keys
|
165
|
+
#
|
130
166
|
# @return [Boolean]
|
167
|
+
#
|
168
|
+
# @api public
|
131
169
|
def strict?
|
132
170
|
options.fetch(:strict, false)
|
133
171
|
end
|
134
172
|
|
135
173
|
# Make the schema intolerant to unknown keys
|
174
|
+
#
|
136
175
|
# @return [Schema]
|
137
|
-
|
138
|
-
|
176
|
+
#
|
177
|
+
# @api public
|
178
|
+
def strict(strict = true)
|
179
|
+
with(strict: strict)
|
139
180
|
end
|
140
181
|
|
141
182
|
# Injects a key transformation function
|
183
|
+
#
|
142
184
|
# @param [#call,nil] proc
|
143
185
|
# @param [#call,nil] block
|
186
|
+
#
|
144
187
|
# @return [Schema]
|
188
|
+
#
|
189
|
+
# @api public
|
145
190
|
def with_key_transform(proc = nil, &block)
|
146
191
|
fn = proc || block
|
147
192
|
|
148
|
-
if fn.nil?
|
149
|
-
raise ArgumentError, "a block or callable argument is required"
|
150
|
-
end
|
193
|
+
raise ArgumentError, 'a block or callable argument is required' if fn.nil?
|
151
194
|
|
152
195
|
handle = Dry::Types::FnContainer.register(fn)
|
153
196
|
with(key_transform_fn: handle)
|
154
197
|
end
|
155
198
|
|
156
199
|
# Whether the schema transforms input keys
|
200
|
+
#
|
157
201
|
# @return [Boolean]
|
202
|
+
#
|
158
203
|
# @api public
|
159
|
-
def
|
204
|
+
def transform_keys?
|
160
205
|
!options[:key_transform_fn].nil?
|
161
206
|
end
|
162
207
|
|
@@ -164,10 +209,13 @@ module Dry
|
|
164
209
|
# @param [{Symbol => Dry::Types::Nominal}] type_map
|
165
210
|
# @param [Hash] meta
|
166
211
|
# @return [Dry::Types::Schema]
|
212
|
+
#
|
167
213
|
# @overload schema(keys)
|
168
214
|
# @param [Array<Dry::Types::Schema::Key>] key List of schema keys
|
169
215
|
# @param [Hash] meta
|
170
216
|
# @return [Dry::Types::Schema]
|
217
|
+
#
|
218
|
+
# @api public
|
171
219
|
def schema(keys_or_map)
|
172
220
|
if keys_or_map.is_a?(::Array)
|
173
221
|
new_keys = keys_or_map
|
@@ -182,6 +230,8 @@ module Dry
|
|
182
230
|
# Iterate over each key type
|
183
231
|
#
|
184
232
|
# @return [Array<Dry::Types::Schema::Key>,Enumerator]
|
233
|
+
#
|
234
|
+
# @api public
|
185
235
|
def each(&block)
|
186
236
|
keys.each(&block)
|
187
237
|
end
|
@@ -189,12 +239,16 @@ module Dry
|
|
189
239
|
# Whether the schema has the given key
|
190
240
|
#
|
191
241
|
# @param [Symbol] name Key name
|
242
|
+
#
|
192
243
|
# @return [Boolean]
|
244
|
+
#
|
245
|
+
# @api public
|
193
246
|
def key?(name)
|
194
247
|
name_key_map.key?(name)
|
195
248
|
end
|
196
249
|
|
197
|
-
# Fetch key type by a key name
|
250
|
+
# Fetch key type by a key name
|
251
|
+
#
|
198
252
|
# Behaves as ::Hash#fetch
|
199
253
|
#
|
200
254
|
# @overload key(name, fallback = Undefined)
|
@@ -206,6 +260,8 @@ module Dry
|
|
206
260
|
# @param [Symbol] name Key name
|
207
261
|
# @param [Proc] block Fallback block, runs if key is missing
|
208
262
|
# @return [Dry::Types::Schema::Key,Object] key type or block value if key is not in schema
|
263
|
+
#
|
264
|
+
# @api public
|
209
265
|
def key(name, fallback = Undefined, &block)
|
210
266
|
if Undefined.equal?(fallback)
|
211
267
|
name_key_map.fetch(name, &block)
|
@@ -215,83 +271,124 @@ module Dry
|
|
215
271
|
end
|
216
272
|
|
217
273
|
# @return [Boolean]
|
274
|
+
#
|
275
|
+
# @api public
|
218
276
|
def constrained?
|
219
277
|
true
|
220
278
|
end
|
221
279
|
|
280
|
+
# @return [Lax]
|
281
|
+
#
|
282
|
+
# @api public
|
283
|
+
def lax
|
284
|
+
Lax.new(schema(keys.map(&:lax)))
|
285
|
+
end
|
286
|
+
|
222
287
|
private
|
223
288
|
|
224
289
|
# @param [Array<Dry::Types::Schema::Keys>] keys
|
290
|
+
#
|
225
291
|
# @return [Dry::Types::Schema]
|
292
|
+
#
|
226
293
|
# @api private
|
227
294
|
def merge_keys(*keys)
|
228
|
-
keys
|
229
|
-
flatten(1)
|
230
|
-
each_with_object({}) { |key, merged| merged[key.name] = key }
|
231
|
-
values
|
295
|
+
keys
|
296
|
+
.flatten(1)
|
297
|
+
.each_with_object({}) { |key, merged| merged[key.name] = key }
|
298
|
+
.values
|
232
299
|
end
|
233
300
|
|
234
|
-
|
301
|
+
# Validate and coerce a hash. Raise an exception on any error
|
302
|
+
#
|
303
|
+
# @api private
|
304
|
+
#
|
305
|
+
# @return [Hash]
|
306
|
+
def resolve_unsafe(hash, options = EMPTY_HASH)
|
235
307
|
result = {}
|
236
308
|
|
237
309
|
hash.each do |key, value|
|
238
|
-
k = transform_key.(key)
|
239
|
-
|
240
|
-
|
241
|
-
|
310
|
+
k = @transform_key.(key)
|
311
|
+
type = @name_key_map[k]
|
312
|
+
|
313
|
+
if type
|
314
|
+
begin
|
315
|
+
result[k] = type.call_unsafe(value)
|
316
|
+
rescue ConstraintError => e
|
317
|
+
raise SchemaError.new(type.name, value, e.result)
|
318
|
+
rescue CoercionError => e
|
319
|
+
raise SchemaError.new(type.name, value, e.message)
|
320
|
+
end
|
242
321
|
elsif strict?
|
243
|
-
raise
|
322
|
+
raise unexpected_keys(hash.keys)
|
244
323
|
end
|
245
324
|
end
|
246
325
|
|
247
|
-
if result.size < keys.size
|
248
|
-
|
326
|
+
resolve_missing_keys(result, options) if result.size < keys.size
|
327
|
+
|
328
|
+
result
|
329
|
+
end
|
330
|
+
|
331
|
+
# Validate and coerce a hash. Call a block and halt on any error
|
332
|
+
#
|
333
|
+
# @api private
|
334
|
+
#
|
335
|
+
# @return [Hash]
|
336
|
+
def resolve_safe(hash, options = EMPTY_HASH, &block)
|
337
|
+
result = {}
|
338
|
+
|
339
|
+
hash.each do |key, value|
|
340
|
+
k = @transform_key.(key)
|
341
|
+
type = @name_key_map[k]
|
342
|
+
|
343
|
+
if type
|
344
|
+
result[k] = type.call_safe(value, &block)
|
345
|
+
elsif strict?
|
346
|
+
yield
|
347
|
+
end
|
249
348
|
end
|
250
349
|
|
350
|
+
resolve_missing_keys(result, options, &block) if result.size < keys.size
|
351
|
+
|
251
352
|
result
|
252
353
|
end
|
253
354
|
|
254
|
-
|
355
|
+
# Try to add missing keys to the hash
|
356
|
+
#
|
357
|
+
# @api private
|
358
|
+
def resolve_missing_keys(hash, options)
|
255
359
|
skip_missing = options.fetch(:skip_missing, false)
|
256
360
|
resolve_defaults = options.fetch(:resolve_defaults, true)
|
257
361
|
|
258
362
|
keys.each do |key|
|
259
|
-
next if
|
363
|
+
next if hash.key?(key.name)
|
260
364
|
|
261
365
|
if key.default? && resolve_defaults
|
262
|
-
|
366
|
+
hash[key.name] = key.call_unsafe(Undefined)
|
263
367
|
elsif key.required? && !skip_missing
|
264
|
-
|
368
|
+
if block_given?
|
369
|
+
return yield
|
370
|
+
else
|
371
|
+
raise missing_key(key.name)
|
372
|
+
end
|
265
373
|
end
|
266
374
|
end
|
267
375
|
end
|
268
376
|
|
269
|
-
# @param
|
270
|
-
#
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
def try_coerce(hash)
|
278
|
-
resolve(hash) do |key, value|
|
279
|
-
yield(key, key.try(value))
|
280
|
-
end
|
377
|
+
# @param hash_keys [Array<Symbol>]
|
378
|
+
#
|
379
|
+
# @return [UnknownKeysError]
|
380
|
+
#
|
381
|
+
# @api private
|
382
|
+
def unexpected_keys(hash_keys)
|
383
|
+
extra_keys = hash_keys.map(&transform_key) - name_key_map.keys
|
384
|
+
UnknownKeysError.new(extra_keys)
|
281
385
|
end
|
282
386
|
|
283
|
-
# @
|
284
|
-
#
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
key.(value)
|
289
|
-
rescue ConstraintError => e
|
290
|
-
raise SchemaError.new(key.name, value, e.result)
|
291
|
-
rescue TypeError, ArgumentError => e
|
292
|
-
raise SchemaError.new(key.name, value, e.message)
|
293
|
-
end
|
294
|
-
end
|
387
|
+
# @return [MissingKeyError]
|
388
|
+
#
|
389
|
+
# @api private
|
390
|
+
def missing_key(key)
|
391
|
+
MissingKeyError.new(key)
|
295
392
|
end
|
296
393
|
end
|
297
394
|
end
|