dry-types 0.15.0 → 1.0.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/.gitignore +1 -0
- data/.rubocop.yml +18 -2
- data/.travis.yml +4 -5
- data/.yardopts +6 -2
- data/CHANGELOG.md +69 -1
- data/Gemfile +3 -0
- data/README.md +2 -1
- data/Rakefile +2 -0
- data/benchmarks/hash_schemas.rb +2 -0
- data/benchmarks/lax_schema.rb +16 -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/dry-types.gemspec +4 -2
- data/lib/dry-types.rb +2 -0
- data/lib/dry/types.rb +51 -13
- data/lib/dry/types/any.rb +21 -10
- data/lib/dry/types/array.rb +11 -1
- data/lib/dry/types/array/member.rb +65 -13
- data/lib/dry/types/builder.rb +48 -4
- data/lib/dry/types/builder_methods.rb +9 -8
- data/lib/dry/types/coercions.rb +71 -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 +73 -27
- data/lib/dry/types/constrained/coercible.rb +36 -6
- data/lib/dry/types/constraints.rb +15 -1
- data/lib/dry/types/constructor.rb +90 -43
- data/lib/dry/types/constructor/function.rb +201 -0
- data/lib/dry/types/container.rb +5 -0
- data/lib/dry/types/core.rb +7 -5
- data/lib/dry/types/decorator.rb +36 -9
- data/lib/dry/types/default.rb +48 -16
- data/lib/dry/types/enum.rb +30 -16
- data/lib/dry/types/errors.rb +73 -7
- data/lib/dry/types/extensions.rb +2 -0
- data/lib/dry/types/extensions/maybe.rb +43 -4
- data/lib/dry/types/fn_container.rb +5 -0
- data/lib/dry/types/hash.rb +22 -3
- data/lib/dry/types/hash/constructor.rb +13 -0
- data/lib/dry/types/inflector.rb +2 -0
- data/lib/dry/types/json.rb +4 -6
- data/lib/dry/types/{safe.rb → lax.rb} +34 -17
- data/lib/dry/types/map.rb +63 -29
- data/lib/dry/types/meta.rb +51 -0
- data/lib/dry/types/module.rb +7 -2
- data/lib/dry/types/nominal.rb +105 -13
- data/lib/dry/types/options.rb +12 -25
- data/lib/dry/types/params.rb +5 -3
- data/lib/dry/types/printable.rb +5 -1
- data/lib/dry/types/printer.rb +58 -57
- data/lib/dry/types/result.rb +26 -0
- data/lib/dry/types/schema.rb +169 -66
- data/lib/dry/types/schema/key.rb +34 -39
- data/lib/dry/types/spec/types.rb +41 -1
- 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 +14 -12
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,38 +155,38 @@ module Dry
|
|
110
155
|
|
111
156
|
[
|
112
157
|
:schema,
|
113
|
-
[
|
114
|
-
keys.map { |key| key.to_ast(meta: meta) },
|
158
|
+
[keys.map { |key| key.to_ast(meta: meta) },
|
115
159
|
opts,
|
116
|
-
meta ? self.meta : EMPTY_HASH
|
117
|
-
]
|
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]
|
176
|
+
#
|
177
|
+
# @api public
|
137
178
|
def strict
|
138
179
|
with(strict: true)
|
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
|
|
@@ -154,7 +199,9 @@ module Dry
|
|
154
199
|
end
|
155
200
|
|
156
201
|
# Whether the schema transforms input keys
|
202
|
+
#
|
157
203
|
# @return [Boolean]
|
204
|
+
#
|
158
205
|
# @api public
|
159
206
|
def trasform_keys?
|
160
207
|
!options[:key_transform_fn].nil?
|
@@ -164,10 +211,13 @@ module Dry
|
|
164
211
|
# @param [{Symbol => Dry::Types::Nominal}] type_map
|
165
212
|
# @param [Hash] meta
|
166
213
|
# @return [Dry::Types::Schema]
|
214
|
+
#
|
167
215
|
# @overload schema(keys)
|
168
216
|
# @param [Array<Dry::Types::Schema::Key>] key List of schema keys
|
169
217
|
# @param [Hash] meta
|
170
218
|
# @return [Dry::Types::Schema]
|
219
|
+
#
|
220
|
+
# @api public
|
171
221
|
def schema(keys_or_map)
|
172
222
|
if keys_or_map.is_a?(::Array)
|
173
223
|
new_keys = keys_or_map
|
@@ -182,6 +232,8 @@ module Dry
|
|
182
232
|
# Iterate over each key type
|
183
233
|
#
|
184
234
|
# @return [Array<Dry::Types::Schema::Key>,Enumerator]
|
235
|
+
#
|
236
|
+
# @api public
|
185
237
|
def each(&block)
|
186
238
|
keys.each(&block)
|
187
239
|
end
|
@@ -189,12 +241,16 @@ module Dry
|
|
189
241
|
# Whether the schema has the given key
|
190
242
|
#
|
191
243
|
# @param [Symbol] name Key name
|
244
|
+
#
|
192
245
|
# @return [Boolean]
|
246
|
+
#
|
247
|
+
# @api public
|
193
248
|
def key?(name)
|
194
249
|
name_key_map.key?(name)
|
195
250
|
end
|
196
251
|
|
197
|
-
# Fetch key type by a key name
|
252
|
+
# Fetch key type by a key name
|
253
|
+
#
|
198
254
|
# Behaves as ::Hash#fetch
|
199
255
|
#
|
200
256
|
# @overload key(name, fallback = Undefined)
|
@@ -206,6 +262,8 @@ module Dry
|
|
206
262
|
# @param [Symbol] name Key name
|
207
263
|
# @param [Proc] block Fallback block, runs if key is missing
|
208
264
|
# @return [Dry::Types::Schema::Key,Object] key type or block value if key is not in schema
|
265
|
+
#
|
266
|
+
# @api public
|
209
267
|
def key(name, fallback = Undefined, &block)
|
210
268
|
if Undefined.equal?(fallback)
|
211
269
|
name_key_map.fetch(name, &block)
|
@@ -215,14 +273,25 @@ module Dry
|
|
215
273
|
end
|
216
274
|
|
217
275
|
# @return [Boolean]
|
276
|
+
#
|
277
|
+
# @api public
|
218
278
|
def constrained?
|
219
279
|
true
|
220
280
|
end
|
221
281
|
|
282
|
+
# @return [Lax]
|
283
|
+
#
|
284
|
+
# @api public
|
285
|
+
def lax
|
286
|
+
Lax.new(schema(keys.map(&:lax)))
|
287
|
+
end
|
288
|
+
|
222
289
|
private
|
223
290
|
|
224
291
|
# @param [Array<Dry::Types::Schema::Keys>] keys
|
292
|
+
#
|
225
293
|
# @return [Dry::Types::Schema]
|
294
|
+
#
|
226
295
|
# @api private
|
227
296
|
def merge_keys(*keys)
|
228
297
|
keys.
|
@@ -231,16 +300,54 @@ module Dry
|
|
231
300
|
values
|
232
301
|
end
|
233
302
|
|
234
|
-
|
303
|
+
# Validate and coerce a hash. Raise an exception on any error
|
304
|
+
#
|
305
|
+
# @api private
|
306
|
+
#
|
307
|
+
# @return [Hash]
|
308
|
+
def resolve_unsafe(hash, options = EMPTY_HASH)
|
235
309
|
result = {}
|
236
310
|
|
237
311
|
hash.each do |key, value|
|
238
|
-
k = transform_key.(key)
|
312
|
+
k = @transform_key.(key)
|
313
|
+
type = @name_key_map[k]
|
314
|
+
|
315
|
+
if type
|
316
|
+
begin
|
317
|
+
result[k] = type.call_unsafe(value)
|
318
|
+
rescue ConstraintError => error
|
319
|
+
raise SchemaError.new(type.name, value, error.result)
|
320
|
+
rescue CoercionError => error
|
321
|
+
raise SchemaError.new(type.name, value, error.message)
|
322
|
+
end
|
323
|
+
elsif strict?
|
324
|
+
raise unexpected_keys(hash.keys)
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
if result.size < keys.size
|
329
|
+
resolve_missing_keys(result, options)
|
330
|
+
end
|
239
331
|
|
240
|
-
|
241
|
-
|
332
|
+
result
|
333
|
+
end
|
334
|
+
|
335
|
+
# Validate and coerce a hash. Call a block and halt on any error
|
336
|
+
#
|
337
|
+
# @api private
|
338
|
+
#
|
339
|
+
# @return [Hash]
|
340
|
+
def resolve_safe(hash, options = EMPTY_HASH, &block)
|
341
|
+
result = {}
|
342
|
+
|
343
|
+
hash.each do |key, value|
|
344
|
+
k = @transform_key.(key)
|
345
|
+
type = @name_key_map[k]
|
346
|
+
|
347
|
+
if type
|
348
|
+
result[k] = type.call_safe(value, &block)
|
242
349
|
elsif strict?
|
243
|
-
|
350
|
+
yield
|
244
351
|
end
|
245
352
|
end
|
246
353
|
|
@@ -251,47 +358,43 @@ module Dry
|
|
251
358
|
result
|
252
359
|
end
|
253
360
|
|
254
|
-
|
361
|
+
# Try to add missing keys to the hash
|
362
|
+
#
|
363
|
+
# @api private
|
364
|
+
def resolve_missing_keys(hash, options)
|
255
365
|
skip_missing = options.fetch(:skip_missing, false)
|
256
366
|
resolve_defaults = options.fetch(:resolve_defaults, true)
|
257
367
|
|
258
368
|
keys.each do |key|
|
259
|
-
next if
|
369
|
+
next if hash.key?(key.name)
|
260
370
|
|
261
371
|
if key.default? && resolve_defaults
|
262
|
-
|
372
|
+
hash[key.name] = key.call_unsafe(Undefined)
|
263
373
|
elsif key.required? && !skip_missing
|
264
|
-
|
374
|
+
if block_given?
|
375
|
+
return yield
|
376
|
+
else
|
377
|
+
raise missing_key(key.name)
|
378
|
+
end
|
265
379
|
end
|
266
380
|
end
|
267
381
|
end
|
268
382
|
|
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
|
383
|
+
# @param hash_keys [Array<Symbol>]
|
384
|
+
#
|
385
|
+
# @return [UnknownKeysError]
|
386
|
+
#
|
387
|
+
# @api private
|
388
|
+
def unexpected_keys(hash_keys)
|
389
|
+
extra_keys = hash_keys.map(&transform_key) - name_key_map.keys
|
390
|
+
UnknownKeysError.new(extra_keys)
|
281
391
|
end
|
282
392
|
|
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
|
393
|
+
# @return [MissingKeyError]
|
394
|
+
#
|
395
|
+
# @api private
|
396
|
+
def missing_key(key)
|
397
|
+
MissingKeyError.new(key)
|
295
398
|
end
|
296
399
|
end
|
297
400
|
end
|