dry-types 0.15.0 → 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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +547 -161
  3. data/LICENSE +17 -17
  4. data/README.md +15 -13
  5. data/dry-types.gemspec +27 -30
  6. data/lib/dry/types/any.rb +23 -12
  7. data/lib/dry/types/array/constructor.rb +32 -0
  8. data/lib/dry/types/array/member.rb +74 -15
  9. data/lib/dry/types/array.rb +18 -2
  10. data/lib/dry/types/builder.rb +118 -22
  11. data/lib/dry/types/builder_methods.rb +46 -16
  12. data/lib/dry/types/coercions/json.rb +43 -7
  13. data/lib/dry/types/coercions/params.rb +117 -32
  14. data/lib/dry/types/coercions.rb +76 -22
  15. data/lib/dry/types/compiler.rb +44 -21
  16. data/lib/dry/types/constrained/coercible.rb +36 -6
  17. data/lib/dry/types/constrained.rb +79 -31
  18. data/lib/dry/types/constraints.rb +18 -4
  19. data/lib/dry/types/constructor/function.rb +216 -0
  20. data/lib/dry/types/constructor/wrapper.rb +94 -0
  21. data/lib/dry/types/constructor.rb +110 -61
  22. data/lib/dry/types/container.rb +6 -1
  23. data/lib/dry/types/core.rb +34 -11
  24. data/lib/dry/types/decorator.rb +38 -17
  25. data/lib/dry/types/default.rb +61 -16
  26. data/lib/dry/types/enum.rb +36 -20
  27. data/lib/dry/types/errors.rb +74 -8
  28. data/lib/dry/types/extensions/maybe.rb +65 -17
  29. data/lib/dry/types/extensions/monads.rb +29 -0
  30. data/lib/dry/types/extensions.rb +7 -1
  31. data/lib/dry/types/fn_container.rb +6 -1
  32. data/lib/dry/types/hash/constructor.rb +17 -4
  33. data/lib/dry/types/hash.rb +32 -20
  34. data/lib/dry/types/inflector.rb +3 -1
  35. data/lib/dry/types/json.rb +18 -16
  36. data/lib/dry/types/lax.rb +75 -0
  37. data/lib/dry/types/map.rb +70 -32
  38. data/lib/dry/types/meta.rb +51 -0
  39. data/lib/dry/types/module.rb +16 -11
  40. data/lib/dry/types/nominal.rb +113 -22
  41. data/lib/dry/types/options.rb +12 -25
  42. data/lib/dry/types/params.rb +39 -25
  43. data/lib/dry/types/predicate_inferrer.rb +238 -0
  44. data/lib/dry/types/predicate_registry.rb +34 -0
  45. data/lib/dry/types/primitive_inferrer.rb +97 -0
  46. data/lib/dry/types/printable.rb +5 -1
  47. data/lib/dry/types/printer.rb +63 -57
  48. data/lib/dry/types/result.rb +29 -3
  49. data/lib/dry/types/schema/key.rb +62 -36
  50. data/lib/dry/types/schema.rb +201 -91
  51. data/lib/dry/types/spec/types.rb +99 -37
  52. data/lib/dry/types/sum.rb +75 -25
  53. data/lib/dry/types/type.rb +49 -0
  54. data/lib/dry/types/version.rb +3 -1
  55. data/lib/dry/types.rb +106 -48
  56. data/lib/dry-types.rb +3 -1
  57. metadata +55 -78
  58. data/.codeclimate.yml +0 -15
  59. data/.gitignore +0 -10
  60. data/.rspec +0 -2
  61. data/.rubocop.yml +0 -43
  62. data/.travis.yml +0 -28
  63. data/.yardopts +0 -5
  64. data/CONTRIBUTING.md +0 -29
  65. data/Gemfile +0 -23
  66. data/Rakefile +0 -20
  67. data/benchmarks/hash_schemas.rb +0 -51
  68. data/lib/dry/types/safe.rb +0 -61
  69. data/log/.gitkeep +0 -0
@@ -1,4 +1,6 @@
1
- require 'dry/types/fn_container'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types/fn_container"
2
4
 
3
5
  module Dry
4
6
  module Types
@@ -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,116 +54,146 @@ module Dry
47
54
  end
48
55
 
49
56
  # @param [Hash] hash
57
+ #
50
58
  # @return [Hash{Symbol => Object}]
51
- def call(hash)
52
- coerce(hash)
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 }
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
- # @param [Hash] hash
86
+ # @param input [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 = {}
74
100
 
75
- begin
76
- result = try_coerce(hash) do |key, key_result|
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
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
- if RUBY_VERSION >= "2.5"
104
- opts = options.slice(:key_transform_fn, :type_transform_fn, :strict)
105
- else
106
- opts = options.select { |k, _|
107
- k == :key_transform_fn || k == :type_transform_fn || k == :strict
108
- }
109
- end
110
-
111
148
  [
112
149
  :schema,
113
- [
114
- keys.map { |key| key.to_ast(meta: meta) },
115
- opts,
116
- meta ? self.meta : EMPTY_HASH
117
- ]
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]
118
153
  ]
119
154
  end
120
155
 
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
156
  # Whether the schema rejects unknown keys
157
+ #
130
158
  # @return [Boolean]
159
+ #
160
+ # @api public
131
161
  def strict?
132
162
  options.fetch(:strict, false)
133
163
  end
134
164
 
135
165
  # Make the schema intolerant to unknown keys
166
+ #
136
167
  # @return [Schema]
137
- def strict
138
- with(strict: true)
168
+ #
169
+ # @api public
170
+ def strict(strict = true)
171
+ with(strict: strict)
139
172
  end
140
173
 
141
- # Injects a key transformation function
174
+ # Inject a key transformation function
175
+ #
142
176
  # @param [#call,nil] proc
143
177
  # @param [#call,nil] block
178
+ #
144
179
  # @return [Schema]
180
+ #
181
+ # @api public
145
182
  def with_key_transform(proc = nil, &block)
146
183
  fn = proc || block
147
184
 
148
- if fn.nil?
149
- raise ArgumentError, "a block or callable argument is required"
150
- end
185
+ raise ArgumentError, "a block or callable argument is required" if fn.nil?
151
186
 
152
187
  handle = Dry::Types::FnContainer.register(fn)
153
188
  with(key_transform_fn: handle)
154
189
  end
155
190
 
156
191
  # Whether the schema transforms input keys
192
+ #
157
193
  # @return [Boolean]
194
+ #
158
195
  # @api public
159
- def trasform_keys?
196
+ def transform_keys?
160
197
  !options[:key_transform_fn].nil?
161
198
  end
162
199
 
@@ -164,10 +201,13 @@ module Dry
164
201
  # @param [{Symbol => Dry::Types::Nominal}] type_map
165
202
  # @param [Hash] meta
166
203
  # @return [Dry::Types::Schema]
204
+ #
167
205
  # @overload schema(keys)
168
206
  # @param [Array<Dry::Types::Schema::Key>] key List of schema keys
169
207
  # @param [Hash] meta
170
208
  # @return [Dry::Types::Schema]
209
+ #
210
+ # @api public
171
211
  def schema(keys_or_map)
172
212
  if keys_or_map.is_a?(::Array)
173
213
  new_keys = keys_or_map
@@ -182,6 +222,8 @@ module Dry
182
222
  # Iterate over each key type
183
223
  #
184
224
  # @return [Array<Dry::Types::Schema::Key>,Enumerator]
225
+ #
226
+ # @api public
185
227
  def each(&block)
186
228
  keys.each(&block)
187
229
  end
@@ -189,12 +231,16 @@ module Dry
189
231
  # Whether the schema has the given key
190
232
  #
191
233
  # @param [Symbol] name Key name
234
+ #
192
235
  # @return [Boolean]
236
+ #
237
+ # @api public
193
238
  def key?(name)
194
239
  name_key_map.key?(name)
195
240
  end
196
241
 
197
- # Fetch key type by a key name.
242
+ # Fetch key type by a key name
243
+ #
198
244
  # Behaves as ::Hash#fetch
199
245
  #
200
246
  # @overload key(name, fallback = Undefined)
@@ -206,6 +252,8 @@ module Dry
206
252
  # @param [Symbol] name Key name
207
253
  # @param [Proc] block Fallback block, runs if key is missing
208
254
  # @return [Dry::Types::Schema::Key,Object] key type or block value if key is not in schema
255
+ #
256
+ # @api public
209
257
  def key(name, fallback = Undefined, &block)
210
258
  if Undefined.equal?(fallback)
211
259
  name_key_map.fetch(name, &block)
@@ -215,83 +263,145 @@ module Dry
215
263
  end
216
264
 
217
265
  # @return [Boolean]
266
+ #
267
+ # @api public
218
268
  def constrained?
219
269
  true
220
270
  end
221
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
+
222
300
  private
223
301
 
224
302
  # @param [Array<Dry::Types::Schema::Keys>] keys
303
+ #
225
304
  # @return [Dry::Types::Schema]
305
+ #
226
306
  # @api private
227
307
  def merge_keys(*keys)
228
- keys.
229
- flatten(1).
230
- each_with_object({}) { |key, merged| merged[key.name] = key }.
231
- values
308
+ keys
309
+ .flatten(1)
310
+ .each_with_object({}) { |key, merged| merged[key.name] = key }
311
+ .values
232
312
  end
233
313
 
234
- def resolve(hash, options = EMPTY_HASH, &block)
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)
235
320
  result = {}
236
321
 
237
322
  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)
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
242
334
  elsif strict?
243
- raise UnknownKeysError.new(*unexpected_keys(hash.keys))
335
+ raise unexpected_keys(hash.keys)
244
336
  end
245
337
  end
246
338
 
247
- if result.size < keys.size
248
- resolve_missing_keys(result, options, &block)
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
249
361
  end
250
362
 
363
+ resolve_missing_keys(result, options, &block) if result.size < keys.size
364
+
251
365
  result
252
366
  end
253
367
 
254
- def resolve_missing_keys(result, options)
368
+ # Try to add missing keys to the hash
369
+ #
370
+ # @api private
371
+ def resolve_missing_keys(hash, options)
255
372
  skip_missing = options.fetch(:skip_missing, false)
256
373
  resolve_defaults = options.fetch(:resolve_defaults, true)
257
374
 
258
375
  keys.each do |key|
259
- next if result.key?(key.name)
376
+ next if hash.key?(key.name)
260
377
 
261
378
  if key.default? && resolve_defaults
262
- result[key.name] = yield(key, Undefined)
379
+ hash[key.name] = key.call_unsafe(Undefined)
263
380
  elsif key.required? && !skip_missing
264
- raise MissingKeyError, key.name
381
+ if block_given?
382
+ return yield
383
+ else
384
+ raise missing_key(key.name)
385
+ end
265
386
  end
266
387
  end
267
388
  end
268
389
 
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
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)
281
398
  end
282
399
 
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
400
+ # @return [MissingKeyError]
401
+ #
402
+ # @api private
403
+ def missing_key(key)
404
+ MissingKeyError.new(key)
295
405
  end
296
406
  end
297
407
  end
@@ -1,102 +1,164 @@
1
- RSpec.shared_examples_for 'Dry::Types::Nominal without primitive' do
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples_for "Dry::Types::Nominal without primitive" do
2
4
  def be_boolean
3
- satisfy { |x| x == true || x == false }
5
+ satisfy { |x| x == true || x == false }
4
6
  end
5
7
 
6
- describe '#constrained?' do
7
- it 'returns a boolean value' do
8
+ describe "#constrained?" do
9
+ it "returns a boolean value" do
8
10
  expect(type.constrained?).to be_boolean
9
11
  end
10
12
  end
11
13
 
12
- describe '#default?' do
13
- it 'returns a boolean value' do
14
+ describe "#default?" do
15
+ it "returns a boolean value" do
14
16
  expect(type.default?).to be_boolean
15
17
  end
16
18
  end
17
19
 
18
- describe '#valid?' do
19
- it 'returns a boolean value' do
20
+ describe "#valid?" do
21
+ it "returns a boolean value" do
20
22
  expect(type.valid?(1)).to be_boolean
21
23
  end
22
24
  end
23
25
 
24
- describe '#eql?' do
25
- it 'has #eql? defined' do
26
+ describe "#eql?" do
27
+ it "has #eql? defined" do
26
28
  expect(type).to eql(type)
27
29
  end
28
30
  end
29
31
 
30
- describe '#==' do
31
- it 'has #== defined' do
32
+ describe "#==" do
33
+ it "has #== defined" do
32
34
  expect(type).to eq(type)
33
35
  end
34
36
  end
35
37
 
36
- describe '#optional?' do
37
- it 'returns a boolean value' do
38
+ describe "#optional?" do
39
+ it "returns a boolean value" do
38
40
  expect(type.optional?).to be_boolean
39
41
  end
40
42
  end
41
43
 
42
- describe '#to_s' do
43
- it 'returns a custom string representation' do
44
- if type.class.name.start_with?('Dry::Types')
45
- expect(type.to_s).to start_with('#<Dry::Types')
46
- end
44
+ describe "#to_s" do
45
+ it "returns a custom string representation" do
46
+ expect(type.to_s).to start_with("#<Dry::Types") if type.class.name.start_with?("Dry::Types")
47
+ end
48
+ end
49
+
50
+ describe "#to_proc" do
51
+ subject(:callable) { type.to_proc }
52
+
53
+ it "converts a type to a proc" do
54
+ expect(callable).to be_a(Proc)
47
55
  end
48
56
  end
49
57
  end
50
58
 
51
- RSpec.shared_examples_for 'Dry::Types::Nominal#meta' do
52
- describe '#meta' do
53
- it 'allows setting meta information' do
54
- with_meta = type.meta(foo: :bar).meta(baz: '1')
59
+ RSpec.shared_examples_for "Dry::Types::Nominal#meta" do
60
+ describe "#meta" do
61
+ it "allows setting meta information" do
62
+ with_meta = type.meta(foo: :bar).meta(baz: "1")
55
63
 
56
64
  expect(with_meta).to be_instance_of(type.class)
57
- expect(with_meta.meta).to eql(foo: :bar, baz: '1')
65
+ expect(with_meta.meta).to eql(foo: :bar, baz: "1")
58
66
  end
59
67
 
60
- it 'equalizes on empty meta' do
68
+ it "equalizes on empty meta" do
61
69
  expect(type).to eql(type.meta({}))
62
70
  end
63
71
 
64
- it 'equalizes on filled meta' do
65
- expect(type).to_not eql(type.meta(i_am: 'different'))
72
+ it "equalizes on filled meta" do
73
+ expect(type).to_not eql(type.meta(i_am: "different"))
66
74
  end
67
75
 
68
- it 'is locally immutable' do
76
+ it "is locally immutable" do
69
77
  expect(type.meta).to be_a ::Hash
70
78
  expect(type.meta).to be_frozen
71
79
  expect(type.meta).not_to have_key :immutable_test
72
- derived = type.with(meta: {immutable_test: 1})
80
+ derived = type.meta(immutable_test: 1)
73
81
  expect(derived.meta).to be_frozen
74
- expect(derived.meta).to eql({immutable_test: 1})
82
+ expect(derived.meta).to eql(immutable_test: 1)
75
83
  expect(type.meta).not_to have_key :immutable_test
76
84
  end
77
85
  end
78
86
 
79
- describe '#pristine' do
80
- it 'erases meta' do
87
+ describe "#pristine" do
88
+ it "erases meta" do
81
89
  expect(type.meta(foo: :bar).pristine).to eql(type)
82
90
  end
83
91
  end
84
92
  end
85
93
 
86
94
  RSpec.shared_examples_for Dry::Types::Nominal do
87
- it_behaves_like 'Dry::Types::Nominal without primitive'
95
+ it_behaves_like "Dry::Types::Nominal without primitive"
88
96
 
89
- describe '#primitive' do
90
- it 'returns a class' do
97
+ describe "#primitive" do
98
+ it "returns a class" do
91
99
  expect(type.primitive).to be_instance_of(Class)
92
100
  end
93
101
  end
94
102
 
95
- describe '#constructor' do
96
- it 'returns a constructor' do
103
+ describe "#constructor" do
104
+ it "returns a constructor" do
97
105
  constructor = type.constructor(&:to_s)
98
106
 
99
107
  expect(constructor).to be_a(Dry::Types::Type)
100
108
  end
101
109
  end
102
110
  end
111
+
112
+ RSpec.shared_examples_for "a constrained type" do |options = {inputs: Object.new}|
113
+ inputs = options[:inputs]
114
+
115
+ let(:fallback) { Object.new }
116
+
117
+ describe "#call" do
118
+ it "yields a block on failure" do
119
+ Array(inputs).each do |input|
120
+ expect(type.(input) { fallback }).to be(fallback)
121
+ end
122
+ end
123
+
124
+ it "throws an error on invalid input" do
125
+ Array(inputs).each do |input|
126
+ expect { type.(input) }.to raise_error(Dry::Types::CoercionError)
127
+ end
128
+ end
129
+ end
130
+
131
+ describe "#constructor" do
132
+ let(:wrapping_constructor) do
133
+ type.constructor { |input, type| type.(input) { fallback } }
134
+ end
135
+
136
+ it "can be wrapped" do
137
+ Array(inputs).each do |input|
138
+ expect(wrapping_constructor.(input)).to be(fallback)
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ RSpec.shared_examples_for "a nominal type" do |inputs: Object.new|
145
+ describe "#call" do
146
+ it "always returns the input back" do
147
+ Array(inputs).each do |input|
148
+ expect(type.(input) { raise }).to be(input)
149
+ expect(type.(input)).to be(input)
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ RSpec.shared_examples_for "a composable constructor" do
156
+ describe "#constructor" do
157
+ it "has aliases for composition" do
158
+ expect(type.method(:append)).to eql(type.method(:constructor))
159
+ expect(type.method(:prepend)).to eql(type.method(:constructor))
160
+ expect(type.method(:<<)).to eql(type.method(:constructor))
161
+ expect(type.method(:>>)).to eql(type.method(:constructor))
162
+ end
163
+ end
164
+ end