dry-types 0.15.0 → 1.5.1

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