dry-types 0.13.2 → 1.5.1

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