dry-types 0.15.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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