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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  3. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  4. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +18 -2
  7. data/.travis.yml +10 -5
  8. data/.yardopts +6 -2
  9. data/CHANGELOG.md +186 -3
  10. data/Gemfile +11 -5
  11. data/README.md +4 -3
  12. data/Rakefile +4 -2
  13. data/benchmarks/hash_schemas.rb +10 -6
  14. data/benchmarks/lax_schema.rb +15 -0
  15. data/benchmarks/profile_invalid_input.rb +15 -0
  16. data/benchmarks/profile_lax_schema_valid.rb +16 -0
  17. data/benchmarks/profile_valid_input.rb +15 -0
  18. data/benchmarks/schema_valid_vs_invalid.rb +21 -0
  19. data/benchmarks/setup.rb +17 -0
  20. data/docsite/source/array-with-member.html.md +13 -0
  21. data/docsite/source/built-in-types.html.md +116 -0
  22. data/docsite/source/constraints.html.md +31 -0
  23. data/docsite/source/custom-types.html.md +93 -0
  24. data/docsite/source/default-values.html.md +91 -0
  25. data/docsite/source/enum.html.md +69 -0
  26. data/docsite/source/getting-started.html.md +57 -0
  27. data/docsite/source/hash-schemas.html.md +169 -0
  28. data/docsite/source/index.html.md +155 -0
  29. data/docsite/source/map.html.md +17 -0
  30. data/docsite/source/optional-values.html.md +96 -0
  31. data/docsite/source/sum.html.md +21 -0
  32. data/dry-types.gemspec +21 -19
  33. data/lib/dry-types.rb +2 -0
  34. data/lib/dry/types.rb +60 -17
  35. data/lib/dry/types/any.rb +21 -10
  36. data/lib/dry/types/array.rb +17 -1
  37. data/lib/dry/types/array/constructor.rb +32 -0
  38. data/lib/dry/types/array/member.rb +72 -13
  39. data/lib/dry/types/builder.rb +49 -5
  40. data/lib/dry/types/builder_methods.rb +43 -16
  41. data/lib/dry/types/coercions.rb +84 -19
  42. data/lib/dry/types/coercions/json.rb +22 -3
  43. data/lib/dry/types/coercions/params.rb +98 -30
  44. data/lib/dry/types/compiler.rb +35 -12
  45. data/lib/dry/types/constrained.rb +78 -27
  46. data/lib/dry/types/constrained/coercible.rb +36 -6
  47. data/lib/dry/types/constraints.rb +15 -1
  48. data/lib/dry/types/constructor.rb +77 -62
  49. data/lib/dry/types/constructor/function.rb +200 -0
  50. data/lib/dry/types/container.rb +5 -0
  51. data/lib/dry/types/core.rb +35 -14
  52. data/lib/dry/types/decorator.rb +37 -10
  53. data/lib/dry/types/default.rb +48 -16
  54. data/lib/dry/types/enum.rb +31 -16
  55. data/lib/dry/types/errors.rb +73 -7
  56. data/lib/dry/types/extensions.rb +6 -0
  57. data/lib/dry/types/extensions/maybe.rb +52 -5
  58. data/lib/dry/types/extensions/monads.rb +29 -0
  59. data/lib/dry/types/fn_container.rb +5 -0
  60. data/lib/dry/types/hash.rb +32 -14
  61. data/lib/dry/types/hash/constructor.rb +16 -3
  62. data/lib/dry/types/inflector.rb +2 -0
  63. data/lib/dry/types/json.rb +7 -5
  64. data/lib/dry/types/{safe.rb → lax.rb} +33 -16
  65. data/lib/dry/types/map.rb +70 -32
  66. data/lib/dry/types/meta.rb +51 -0
  67. data/lib/dry/types/module.rb +10 -5
  68. data/lib/dry/types/nominal.rb +105 -14
  69. data/lib/dry/types/options.rb +12 -25
  70. data/lib/dry/types/params.rb +14 -3
  71. data/lib/dry/types/predicate_inferrer.rb +197 -0
  72. data/lib/dry/types/predicate_registry.rb +34 -0
  73. data/lib/dry/types/primitive_inferrer.rb +97 -0
  74. data/lib/dry/types/printable.rb +5 -1
  75. data/lib/dry/types/printer.rb +70 -64
  76. data/lib/dry/types/result.rb +26 -0
  77. data/lib/dry/types/schema.rb +177 -80
  78. data/lib/dry/types/schema/key.rb +48 -35
  79. data/lib/dry/types/spec/types.rb +43 -6
  80. data/lib/dry/types/sum.rb +70 -21
  81. data/lib/dry/types/type.rb +49 -0
  82. data/lib/dry/types/version.rb +3 -1
  83. metadata +91 -62
@@ -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,53 +155,53 @@ module Dry
110
155
 
111
156
  [
112
157
  :schema,
113
- [
114
- keys.map { |key| key.to_ast(meta: meta) },
115
- opts,
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
- def strict
138
- with(strict: true)
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 trasform_keys?
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
- def resolve(hash, options = EMPTY_HASH, &block)
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
- if name_key_map.key?(k)
241
- result[k] = yield(name_key_map[k], value)
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 UnknownKeysError.new(*unexpected_keys(hash.keys))
322
+ raise unexpected_keys(hash.keys)
244
323
  end
245
324
  end
246
325
 
247
- if result.size < keys.size
248
- resolve_missing_keys(result, options, &block)
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
- def resolve_missing_keys(result, options)
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 result.key?(key.name)
363
+ next if hash.key?(key.name)
260
364
 
261
365
  if key.default? && resolve_defaults
262
- result[key.name] = yield(key, Undefined)
366
+ hash[key.name] = key.call_unsafe(Undefined)
263
367
  elsif key.required? && !skip_missing
264
- raise MissingKeyError, key.name
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 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
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
- # @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
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