dry-types 0.13.2 → 1.5.1

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 (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