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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +18 -2
  4. data/.travis.yml +4 -5
  5. data/.yardopts +6 -2
  6. data/CHANGELOG.md +69 -1
  7. data/Gemfile +3 -0
  8. data/README.md +2 -1
  9. data/Rakefile +2 -0
  10. data/benchmarks/hash_schemas.rb +2 -0
  11. data/benchmarks/lax_schema.rb +16 -0
  12. data/benchmarks/profile_invalid_input.rb +15 -0
  13. data/benchmarks/profile_lax_schema_valid.rb +16 -0
  14. data/benchmarks/profile_valid_input.rb +15 -0
  15. data/benchmarks/schema_valid_vs_invalid.rb +21 -0
  16. data/benchmarks/setup.rb +17 -0
  17. data/dry-types.gemspec +4 -2
  18. data/lib/dry-types.rb +2 -0
  19. data/lib/dry/types.rb +51 -13
  20. data/lib/dry/types/any.rb +21 -10
  21. data/lib/dry/types/array.rb +11 -1
  22. data/lib/dry/types/array/member.rb +65 -13
  23. data/lib/dry/types/builder.rb +48 -4
  24. data/lib/dry/types/builder_methods.rb +9 -8
  25. data/lib/dry/types/coercions.rb +71 -19
  26. data/lib/dry/types/coercions/json.rb +22 -3
  27. data/lib/dry/types/coercions/params.rb +98 -30
  28. data/lib/dry/types/compiler.rb +35 -12
  29. data/lib/dry/types/constrained.rb +73 -27
  30. data/lib/dry/types/constrained/coercible.rb +36 -6
  31. data/lib/dry/types/constraints.rb +15 -1
  32. data/lib/dry/types/constructor.rb +90 -43
  33. data/lib/dry/types/constructor/function.rb +201 -0
  34. data/lib/dry/types/container.rb +5 -0
  35. data/lib/dry/types/core.rb +7 -5
  36. data/lib/dry/types/decorator.rb +36 -9
  37. data/lib/dry/types/default.rb +48 -16
  38. data/lib/dry/types/enum.rb +30 -16
  39. data/lib/dry/types/errors.rb +73 -7
  40. data/lib/dry/types/extensions.rb +2 -0
  41. data/lib/dry/types/extensions/maybe.rb +43 -4
  42. data/lib/dry/types/fn_container.rb +5 -0
  43. data/lib/dry/types/hash.rb +22 -3
  44. data/lib/dry/types/hash/constructor.rb +13 -0
  45. data/lib/dry/types/inflector.rb +2 -0
  46. data/lib/dry/types/json.rb +4 -6
  47. data/lib/dry/types/{safe.rb → lax.rb} +34 -17
  48. data/lib/dry/types/map.rb +63 -29
  49. data/lib/dry/types/meta.rb +51 -0
  50. data/lib/dry/types/module.rb +7 -2
  51. data/lib/dry/types/nominal.rb +105 -13
  52. data/lib/dry/types/options.rb +12 -25
  53. data/lib/dry/types/params.rb +5 -3
  54. data/lib/dry/types/printable.rb +5 -1
  55. data/lib/dry/types/printer.rb +58 -57
  56. data/lib/dry/types/result.rb +26 -0
  57. data/lib/dry/types/schema.rb +169 -66
  58. data/lib/dry/types/schema/key.rb +34 -39
  59. data/lib/dry/types/spec/types.rb +41 -1
  60. data/lib/dry/types/sum.rb +70 -21
  61. data/lib/dry/types/type.rb +49 -0
  62. data/lib/dry/types/version.rb +3 -1
  63. metadata +14 -12
@@ -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
@@ -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
- def call(hash)
52
- coerce(hash)
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
- coerce(hash, options)
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
- def try(hash)
71
- if hash.is_a?(::Hash)
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
- begin
76
- result = try_coerce(hash) do |key, key_result|
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
- output[key.name] = key_result.input
110
+ elsif strict?
111
+ success = false
112
+ end
113
+ end
79
114
 
80
- key_result
115
+ if output.size < keys.size
116
+ resolve_missing_keys(output, options) do
117
+ success = false
81
118
  end
82
- rescue ConstraintError, UnknownKeysError, SchemaError, MissingKeyError => e
83
- success = false
84
- result = e
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
- success = false
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 success
133
+ if failure.nil?
93
134
  success(output)
135
+ elsif block_given?
136
+ yield(failure)
94
137
  else
95
- failure = failure(output, result)
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
- def resolve(hash, options = EMPTY_HASH, &block)
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
- if name_key_map.key?(k)
241
- result[k] = yield(name_key_map[k], value)
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
- raise UnknownKeysError.new(*unexpected_keys(hash.keys))
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
- def resolve_missing_keys(result, options)
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 result.key?(key.name)
369
+ next if hash.key?(key.name)
260
370
 
261
371
  if key.default? && resolve_defaults
262
- result[key.name] = yield(key, Undefined)
372
+ hash[key.name] = key.call_unsafe(Undefined)
263
373
  elsif key.required? && !skip_missing
264
- raise MissingKeyError, key.name
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 keys [Array<Symbol>]
270
- # @return [Array<Symbol>]
271
- def unexpected_keys(keys)
272
- keys.map(&transform_key) - name_key_map.keys
273
- end
274
-
275
- # @param [Hash] hash
276
- # @return [Hash{Symbol => Object}]
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
- # @param [Hash] hash
284
- # @return [Hash{Symbol => Object}]
285
- def coerce(hash, options = EMPTY_HASH)
286
- resolve(hash, options) do |key, value|
287
- begin
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