sorbet-runtime 0.0.1.pre.prealpha → 0.4.4253
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.
- checksums.yaml +5 -5
- data/lib/sorbet-runtime.rb +100 -0
- data/lib/types/_types.rb +245 -0
- data/lib/types/abstract_utils.rb +50 -0
- data/lib/types/boolean.rb +8 -0
- data/lib/types/compatibility_patches.rb +37 -0
- data/lib/types/configuration.rb +368 -0
- data/lib/types/generic.rb +23 -0
- data/lib/types/helpers.rb +31 -0
- data/lib/types/interface_wrapper.rb +158 -0
- data/lib/types/private/abstract/data.rb +36 -0
- data/lib/types/private/abstract/declare.rb +39 -0
- data/lib/types/private/abstract/hooks.rb +43 -0
- data/lib/types/private/abstract/validate.rb +128 -0
- data/lib/types/private/casts.rb +22 -0
- data/lib/types/private/class_utils.rb +102 -0
- data/lib/types/private/decl_state.rb +18 -0
- data/lib/types/private/error_handler.rb +37 -0
- data/lib/types/private/methods/_methods.rb +344 -0
- data/lib/types/private/methods/call_validation.rb +1177 -0
- data/lib/types/private/methods/decl_builder.rb +275 -0
- data/lib/types/private/methods/modes.rb +18 -0
- data/lib/types/private/methods/signature.rb +196 -0
- data/lib/types/private/methods/signature_validation.rb +232 -0
- data/lib/types/private/mixins/mixins.rb +27 -0
- data/lib/types/private/runtime_levels.rb +41 -0
- data/lib/types/private/types/not_typed.rb +23 -0
- data/lib/types/private/types/string_holder.rb +26 -0
- data/lib/types/private/types/void.rb +33 -0
- data/lib/types/profile.rb +27 -0
- data/lib/types/props/_props.rb +165 -0
- data/lib/types/props/constructor.rb +20 -0
- data/lib/types/props/custom_type.rb +84 -0
- data/lib/types/props/decorator.rb +826 -0
- data/lib/types/props/errors.rb +8 -0
- data/lib/types/props/optional.rb +73 -0
- data/lib/types/props/plugin.rb +15 -0
- data/lib/types/props/pretty_printable.rb +106 -0
- data/lib/types/props/serializable.rb +376 -0
- data/lib/types/props/type_validation.rb +98 -0
- data/lib/types/props/utils.rb +49 -0
- data/lib/types/props/weak_constructor.rb +30 -0
- data/lib/types/runtime_profiled.rb +36 -0
- data/lib/types/sig.rb +28 -0
- data/lib/types/struct.rb +8 -0
- data/lib/types/types/base.rb +141 -0
- data/lib/types/types/class_of.rb +38 -0
- data/lib/types/types/enum.rb +42 -0
- data/lib/types/types/fixed_array.rb +60 -0
- data/lib/types/types/fixed_hash.rb +59 -0
- data/lib/types/types/intersection.rb +36 -0
- data/lib/types/types/noreturn.rb +25 -0
- data/lib/types/types/proc.rb +51 -0
- data/lib/types/types/self_type.rb +31 -0
- data/lib/types/types/simple.rb +33 -0
- data/lib/types/types/type_member.rb +7 -0
- data/lib/types/types/type_parameter.rb +23 -0
- data/lib/types/types/type_template.rb +7 -0
- data/lib/types/types/type_variable.rb +31 -0
- data/lib/types/types/typed_array.rb +20 -0
- data/lib/types/types/typed_enumerable.rb +141 -0
- data/lib/types/types/typed_enumerator.rb +22 -0
- data/lib/types/types/typed_hash.rb +29 -0
- data/lib/types/types/typed_range.rb +22 -0
- data/lib/types/types/typed_set.rb +22 -0
- data/lib/types/types/union.rb +59 -0
- data/lib/types/types/untyped.rb +25 -0
- data/lib/types/utils.rb +223 -0
- metadata +122 -15
@@ -0,0 +1,368 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module T::Configuration
|
5
|
+
# Set a handler to handle `TypeError`s raised by any in-line type assertions,
|
6
|
+
# including `T.must`, `T.let`, `T.cast`, and `T.assert_type!`.
|
7
|
+
#
|
8
|
+
# By default, any `TypeError`s detected by this gem will be raised. Setting
|
9
|
+
# inline_type_error_handler to an object that implements :call (e.g. proc or
|
10
|
+
# lambda) allows users to customize the behavior when a `TypeError` is
|
11
|
+
# raised on any inline type assertion.
|
12
|
+
#
|
13
|
+
# @param [Lambda, Proc, Object, nil] value Proc that handles the error (pass
|
14
|
+
# nil to reset to default behavior)
|
15
|
+
#
|
16
|
+
# Parameters passed to value.call:
|
17
|
+
#
|
18
|
+
# @param [TypeError] error TypeError that was raised
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# T::Configuration.inline_type_error_handler = lambda do |error|
|
22
|
+
# puts error.message
|
23
|
+
# end
|
24
|
+
def self.inline_type_error_handler=(value)
|
25
|
+
validate_lambda_given!(value)
|
26
|
+
@inline_type_error_handler = value
|
27
|
+
end
|
28
|
+
|
29
|
+
private_class_method def self.inline_type_error_handler_default(error)
|
30
|
+
raise error
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.inline_type_error_handler(error)
|
34
|
+
if @inline_type_error_handler
|
35
|
+
@inline_type_error_handler.call(error)
|
36
|
+
else
|
37
|
+
inline_type_error_handler_default(error)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Set a handler to handle errors that occur when the builder methods in the
|
42
|
+
# body of a sig are executed. The sig builder methods are inside a proc so
|
43
|
+
# that they can be lazily evaluated the first time the method being sig'd is
|
44
|
+
# called.
|
45
|
+
#
|
46
|
+
# By default, improper use of the builder methods within the body of a sig
|
47
|
+
# cause an ArgumentError to be raised. Setting sig_builder_error_handler to an
|
48
|
+
# object that implements :call (e.g. proc or lambda) allows users to
|
49
|
+
# customize the behavior when a sig can't be built for some reason.
|
50
|
+
#
|
51
|
+
# @param [Lambda, Proc, Object, nil] value Proc that handles the error (pass
|
52
|
+
# nil to reset to default behavior)
|
53
|
+
#
|
54
|
+
# Parameters passed to value.call:
|
55
|
+
#
|
56
|
+
# @param [StandardError] error The error that was raised
|
57
|
+
# @param [Thread::Backtrace::Location] location Location of the error
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# T::Configuration.sig_builder_error_handler = lambda do |error, location|
|
61
|
+
# puts error.message
|
62
|
+
# end
|
63
|
+
def self.sig_builder_error_handler=(value)
|
64
|
+
validate_lambda_given!(value)
|
65
|
+
@sig_builder_error_handler = value
|
66
|
+
end
|
67
|
+
|
68
|
+
private_class_method def self.sig_builder_error_handler_default(error, location)
|
69
|
+
T::Private::Methods.sig_error(location, error.message)
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.sig_builder_error_handler(error, location)
|
73
|
+
if @sig_builder_error_handler
|
74
|
+
@sig_builder_error_handler.call(error, location)
|
75
|
+
else
|
76
|
+
sig_builder_error_handler_default(error, location)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Set a handler to handle sig validation errors.
|
81
|
+
#
|
82
|
+
# Sig validation errors include things like abstract checks, override checks,
|
83
|
+
# and type compatibility of arguments. They happen after a sig has been
|
84
|
+
# successfully built, but the built sig is incompatible with other sigs in
|
85
|
+
# some way.
|
86
|
+
#
|
87
|
+
# By default, sig validation errors cause an exception to be raised. One
|
88
|
+
# exception is for `generated` sigs, for which a message will be logged
|
89
|
+
# instead of raising. Setting sig_validation_error_handler to an object that
|
90
|
+
# implements :call (e.g. proc or lambda) allows users to customize the
|
91
|
+
# behavior when a method signature's build fails.
|
92
|
+
#
|
93
|
+
# @param [Lambda, Proc, Object, nil] value Proc that handles the error (pass
|
94
|
+
# nil to reset to default behavior)
|
95
|
+
#
|
96
|
+
# Parameters passed to value.call:
|
97
|
+
#
|
98
|
+
# @param [StandardError] error The error that was raised
|
99
|
+
# @param [Hash] opts A hash containing contextual information on the error:
|
100
|
+
# @option opts [Method, UnboundMethod] :method Method on which the signature build failed
|
101
|
+
# @option opts [T::Private::Methods::Declaration] :declaration Method
|
102
|
+
# signature declaration struct
|
103
|
+
# @option opts [T::Private::Methods::Signature, nil] :signature Signature
|
104
|
+
# that failed (nil if sig build failed before Signature initialization)
|
105
|
+
# @option opts [T::Private::Methods::Signature, nil] :super_signature Super
|
106
|
+
# method's signature (nil if method is not an override or super method
|
107
|
+
# does not have a method signature)
|
108
|
+
#
|
109
|
+
# @example
|
110
|
+
# T::Configuration.sig_validation_error_handler = lambda do |error, opts|
|
111
|
+
# puts error.message
|
112
|
+
# end
|
113
|
+
def self.sig_validation_error_handler=(value)
|
114
|
+
validate_lambda_given!(value)
|
115
|
+
@sig_validation_error_handler = value
|
116
|
+
end
|
117
|
+
|
118
|
+
private_class_method def self.sig_validation_error_handler_default(error, opts)
|
119
|
+
# if this method overrides a generated signature, report that one instead
|
120
|
+
bad_method = opts[:method]
|
121
|
+
if !opts[:declaration].generated
|
122
|
+
super_signature = opts[:super_signature]
|
123
|
+
raise error if !super_signature&.generated
|
124
|
+
bad_method = super_signature.method
|
125
|
+
end
|
126
|
+
|
127
|
+
method_file, method_line = bad_method.source_location
|
128
|
+
T::Configuration.log_info_handler(
|
129
|
+
"SIG-DECLARE-FAILED",
|
130
|
+
{
|
131
|
+
definition_file: method_file,
|
132
|
+
definition_line: method_line,
|
133
|
+
kind: "Delete",
|
134
|
+
message: error.message,
|
135
|
+
},
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.sig_validation_error_handler(error, opts)
|
140
|
+
if @sig_validation_error_handler
|
141
|
+
@sig_validation_error_handler.call(error, opts)
|
142
|
+
else
|
143
|
+
sig_validation_error_handler_default(error, opts)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Set a handler for type errors that result from calling a method.
|
148
|
+
#
|
149
|
+
# By default, errors from calling a method cause an exception to be raised.
|
150
|
+
# One exception is for `generated` sigs, for which a message will be logged
|
151
|
+
# instead of raising. Setting call_validation_error_handler to an object that
|
152
|
+
# implements :call (e.g. proc or lambda) allows users to customize the
|
153
|
+
# behavior when a method is called with invalid parameters, or returns an
|
154
|
+
# invalid value.
|
155
|
+
#
|
156
|
+
# @param [Lambda, Proc, Object, nil] value Proc that handles the error
|
157
|
+
# report (pass nil to reset to default behavior)
|
158
|
+
#
|
159
|
+
# Parameters passed to value.call:
|
160
|
+
#
|
161
|
+
# @param [T::Private::Methods::Signature] signature Signature that failed
|
162
|
+
# @param [Hash] opts A hash containing contextual information on the error:
|
163
|
+
# @option opts [String] :message Error message
|
164
|
+
# @option opts [String] :kind One of:
|
165
|
+
# ['Parameter', 'Block parameter', 'Return value']
|
166
|
+
# @option opts [Symbol] :name Param or block param name (nil for return
|
167
|
+
# value)
|
168
|
+
# @option opts [Object] :type Expected param/return value type
|
169
|
+
# @option opts [Object] :value Actual param/return value
|
170
|
+
# @option opts [Thread::Backtrace::Location] :location Location of the
|
171
|
+
# caller
|
172
|
+
#
|
173
|
+
# @example
|
174
|
+
# T::Configuration.call_validation_error_handler = lambda do |signature, opts|
|
175
|
+
# puts opts[:message]
|
176
|
+
# end
|
177
|
+
def self.call_validation_error_handler=(value)
|
178
|
+
validate_lambda_given!(value)
|
179
|
+
@call_validation_error_handler = value
|
180
|
+
end
|
181
|
+
|
182
|
+
private_class_method def self.call_validation_error_handler_default(signature, opts)
|
183
|
+
method_file, method_line = signature.method.source_location
|
184
|
+
location = opts[:location]
|
185
|
+
suffix = "Caller: #{location.path}:#{location.lineno}\n" \
|
186
|
+
"Definition: #{method_file}:#{method_line}"
|
187
|
+
|
188
|
+
error_message = "#{opts[:kind]}#{opts[:name] ? " '#{opts[:name]}'" : ''}: #{opts[:message]}\n#{suffix}"
|
189
|
+
|
190
|
+
if signature.generated
|
191
|
+
got = opts[:value].class
|
192
|
+
got = T.unsafe(T::Enumerable[T.untyped]).describe_obj(opts[:value]) if got < Enumerable
|
193
|
+
T::Configuration.log_info_handler(
|
194
|
+
"SIG-CHECK-FAILED",
|
195
|
+
{
|
196
|
+
caller_file: location.path,
|
197
|
+
caller_line: location.lineno,
|
198
|
+
definition_file: method_file,
|
199
|
+
definition_line: method_line,
|
200
|
+
kind: opts[:kind],
|
201
|
+
name: opts[:name],
|
202
|
+
expected: opts[:type].name,
|
203
|
+
got: got,
|
204
|
+
},
|
205
|
+
)
|
206
|
+
elsif signature.soft_notify
|
207
|
+
T::Configuration.soft_assert_handler(
|
208
|
+
"TypeError: #{error_message}",
|
209
|
+
{notify: signature.soft_notify}
|
210
|
+
)
|
211
|
+
else
|
212
|
+
begin
|
213
|
+
raise TypeError.new(error_message)
|
214
|
+
rescue TypeError => e # raise into rescue to ensure e.backtrace is populated
|
215
|
+
T::Private::ErrorHandler.handle_inline_type_error(e)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def self.call_validation_error_handler(signature, opts)
|
221
|
+
if @call_validation_error_handler
|
222
|
+
@call_validation_error_handler.call(signature, opts)
|
223
|
+
else
|
224
|
+
call_validation_error_handler_default(signature, opts)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Set a handler for logging
|
229
|
+
#
|
230
|
+
# @param [Lambda, Proc, Object, nil] value Proc that handles the error
|
231
|
+
# report (pass nil to reset to default behavior)
|
232
|
+
#
|
233
|
+
# Parameters passed to value.call:
|
234
|
+
#
|
235
|
+
# @param [String] str Message to be logged
|
236
|
+
# @param [Hash] extra A hash containing additional parameters to be passed along to the logger.
|
237
|
+
#
|
238
|
+
# @example
|
239
|
+
# T::Configuration.log_info_handler = lambda do |str, extra|
|
240
|
+
# puts "#{str}, context: #{extra}"
|
241
|
+
# end
|
242
|
+
def self.log_info_handler=(value)
|
243
|
+
validate_lambda_given!(value)
|
244
|
+
@log_info_handler = value
|
245
|
+
end
|
246
|
+
|
247
|
+
private_class_method def self.log_info_handler_default(str, extra)
|
248
|
+
puts "#{str}, extra: #{extra}" # rubocop:disable PrisonGuard/NoBarePuts
|
249
|
+
end
|
250
|
+
|
251
|
+
def self.log_info_handler(str, extra)
|
252
|
+
if @log_info_handler
|
253
|
+
@log_info_handler.call(str, extra)
|
254
|
+
else
|
255
|
+
log_info_handler_default(str, extra)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Set a handler for soft assertions
|
260
|
+
#
|
261
|
+
# These generally shouldn't stop execution of the program, but rather inform
|
262
|
+
# some party of the assertion to action on later.
|
263
|
+
#
|
264
|
+
# @param [Lambda, Proc, Object, nil] value Proc that handles the error
|
265
|
+
# report (pass nil to reset to default behavior)
|
266
|
+
#
|
267
|
+
# Parameters passed to value.call:
|
268
|
+
#
|
269
|
+
# @param [String] str Assertion message
|
270
|
+
# @param [Hash] extra A hash containing additional parameters to be passed along to the handler.
|
271
|
+
#
|
272
|
+
# @example
|
273
|
+
# T::Configuration.soft_assert_handler = lambda do |str, extra|
|
274
|
+
# puts "#{str}, context: #{extra}"
|
275
|
+
# end
|
276
|
+
def self.soft_assert_handler=(value)
|
277
|
+
validate_lambda_given!(value)
|
278
|
+
@soft_assert_handler = value
|
279
|
+
end
|
280
|
+
|
281
|
+
private_class_method def self.soft_assert_handler_default(str, extra)
|
282
|
+
puts "#{str}, extra: #{extra}" # rubocop:disable PrisonGuard/NoBarePuts
|
283
|
+
end
|
284
|
+
|
285
|
+
def self.soft_assert_handler(str, extra)
|
286
|
+
if @soft_assert_handler
|
287
|
+
@soft_assert_handler.call(str, extra)
|
288
|
+
else
|
289
|
+
soft_assert_handler_default(str, extra)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Set a handler for hard assertions
|
294
|
+
#
|
295
|
+
# These generally should stop execution of the program, and optionally inform
|
296
|
+
# some party of the assertion.
|
297
|
+
#
|
298
|
+
# @param [Lambda, Proc, Object, nil] value Proc that handles the error
|
299
|
+
# report (pass nil to reset to default behavior)
|
300
|
+
#
|
301
|
+
# Parameters passed to value.call:
|
302
|
+
#
|
303
|
+
# @param [String] str Assertion message
|
304
|
+
# @param [Hash] extra A hash containing additional parameters to be passed along to the handler.
|
305
|
+
#
|
306
|
+
# @example
|
307
|
+
# T::Configuration.hard_assert_handler = lambda do |str, extra|
|
308
|
+
# raise "#{str}, context: #{extra}"
|
309
|
+
# end
|
310
|
+
def self.hard_assert_handler=(value)
|
311
|
+
validate_lambda_given!(value)
|
312
|
+
@hard_assert_handler = value
|
313
|
+
end
|
314
|
+
|
315
|
+
private_class_method def self.hard_assert_handler_default(str, _)
|
316
|
+
raise str
|
317
|
+
end
|
318
|
+
|
319
|
+
def self.hard_assert_handler(str, extra)
|
320
|
+
if @hard_assert_handler
|
321
|
+
@hard_assert_handler.call(str, extra)
|
322
|
+
else
|
323
|
+
hard_assert_handler_default(str, extra)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# Set a list of class strings that are to be considered scalar.
|
328
|
+
# (pass nil to reset to default behavior)
|
329
|
+
#
|
330
|
+
# @param [String] value Class name.
|
331
|
+
#
|
332
|
+
# @example
|
333
|
+
# T::Configuration.scalar_types = ["NilClass", "TrueClass", "FalseClass", ...]
|
334
|
+
def self.scalar_types=(values)
|
335
|
+
if values.nil?
|
336
|
+
@scalar_tyeps = values
|
337
|
+
else
|
338
|
+
bad_values = values.select {|v| v.class != String}
|
339
|
+
unless bad_values.empty?
|
340
|
+
raise ArgumentError.new("Provided values must all be class name strings.")
|
341
|
+
end
|
342
|
+
|
343
|
+
@scalar_types = Set.new(values).freeze
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
@default_scalar_types = Set.new(%w{
|
348
|
+
NilClass
|
349
|
+
TrueClass
|
350
|
+
FalseClass
|
351
|
+
Integer
|
352
|
+
Float
|
353
|
+
String
|
354
|
+
Symbol
|
355
|
+
Time
|
356
|
+
}).freeze
|
357
|
+
|
358
|
+
def self.scalar_types
|
359
|
+
@scalar_types || @default_scalar_types
|
360
|
+
end
|
361
|
+
|
362
|
+
|
363
|
+
private_class_method def self.validate_lambda_given!(value)
|
364
|
+
if !value.nil? && !value.respond_to?(:call)
|
365
|
+
raise ArgumentError.new("Provided value must respond to :call")
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
# Use as a mixin with extend (`extend T::Generic`).
|
5
|
+
# Docs at https://hackpad.corp.stripe.com/Type-Validation-in-pay-server-1JaoTHir5Mo.
|
6
|
+
module T::Generic
|
7
|
+
include T::Helpers
|
8
|
+
include Kernel
|
9
|
+
|
10
|
+
### Class/Module Helpers ###
|
11
|
+
|
12
|
+
def [](*types)
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def type_member(variance=:invariant, fixed: nil)
|
17
|
+
T::Types::TypeMember.new(variance) # rubocop:disable PrisonGuard/UseOpusTypesShortcut
|
18
|
+
end
|
19
|
+
|
20
|
+
def type_template(variance=:invariant, fixed: nil)
|
21
|
+
T::Types::TypeTemplate.new(variance) # rubocop:disable PrisonGuard/UseOpusTypesShortcut
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: true
|
3
|
+
|
4
|
+
# Use as a mixin with extend (`extend T::Helpers`).
|
5
|
+
# Docs at https://confluence.corp.stripe.com/display/PRODINFRA/Ruby+Types
|
6
|
+
module T::Helpers
|
7
|
+
Private = T::Private
|
8
|
+
|
9
|
+
### Class/Module Helpers ###
|
10
|
+
|
11
|
+
def abstract!
|
12
|
+
Private::Abstract::Declare.declare_abstract(self, type: :abstract)
|
13
|
+
end
|
14
|
+
|
15
|
+
def interface!
|
16
|
+
Private::Abstract::Declare.declare_abstract(self, type: :interface)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Causes a mixin to also mix in class methods from the named module.
|
20
|
+
#
|
21
|
+
# Nearly equivalent to
|
22
|
+
#
|
23
|
+
# def self.included(other)
|
24
|
+
# other.extend(mod)
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# Except that it is statically analyzed by sorbet.
|
28
|
+
def mixes_in_class_methods(mod)
|
29
|
+
Private::Mixins.declare_mixes_in_class_methods(self, mod)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: false
|
3
|
+
|
4
|
+
# Wraps an object, exposing only the methods defined on a given class/module. The idea is that, in
|
5
|
+
# the absence of a static type checker that would prevent you from calling non-Bar methods on a
|
6
|
+
# variable of type Bar, we can use these wrappers as a way of enforcing it at runtime.
|
7
|
+
#
|
8
|
+
# Once we ship static type checking, we should get rid of this entirely.
|
9
|
+
class T::InterfaceWrapper
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
module Helpers
|
13
|
+
def wrap_instance(obj)
|
14
|
+
T::InterfaceWrapper.wrap_instance(obj, self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def wrap_instances(arr)
|
18
|
+
T::InterfaceWrapper.wrap_instances(arr, self)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private_class_method :new # use `wrap_instance`
|
23
|
+
|
24
|
+
def self.wrap_instance(obj, interface_mod)
|
25
|
+
wrapper = wrapped_dynamic_cast(obj, interface_mod)
|
26
|
+
if wrapper.nil?
|
27
|
+
raise "#{obj.class} cannot be cast to #{interface_mod}"
|
28
|
+
end
|
29
|
+
wrapper
|
30
|
+
end
|
31
|
+
|
32
|
+
sig do
|
33
|
+
params(
|
34
|
+
arr: Array,
|
35
|
+
interface_mod: T.untyped
|
36
|
+
)
|
37
|
+
.returns(Array)
|
38
|
+
end
|
39
|
+
def self.wrap_instances(arr, interface_mod)
|
40
|
+
arr.map {|instance| self.wrap_instance(instance, interface_mod)}
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(target_obj, interface_mod)
|
44
|
+
if target_obj.is_a?(T::InterfaceWrapper)
|
45
|
+
# wrapped_dynamic_cast should guarantee this never happens.
|
46
|
+
raise "Unexpected: wrapping a wrapper. Please report to #dev-productivity."
|
47
|
+
end
|
48
|
+
|
49
|
+
if !target_obj.is_a?(interface_mod)
|
50
|
+
# wrapped_dynamic_cast should guarantee this never happens.
|
51
|
+
raise "Unexpected: `is_a?` failed. Please report to #dev-productivity."
|
52
|
+
end
|
53
|
+
|
54
|
+
if target_obj.class == interface_mod
|
55
|
+
# wrapped_dynamic_cast should guarantee this never happens.
|
56
|
+
raise "Unexpected: exact class match. Please report to #dev-productivity."
|
57
|
+
end
|
58
|
+
|
59
|
+
@target_obj = target_obj
|
60
|
+
@interface_mod = interface_mod
|
61
|
+
self_methods = self.class.self_methods
|
62
|
+
|
63
|
+
# If perf becomes an issue, we can define these on an anonymous subclass, and keep a cache
|
64
|
+
# so we only need to do it once per unique `interface_mod`
|
65
|
+
T::Utils.methods_excluding_object(interface_mod).each do |method_name|
|
66
|
+
if self_methods.include?(method_name)
|
67
|
+
raise "interface_mod has a method that conflicts with #{self.class}: #{method_name}"
|
68
|
+
end
|
69
|
+
|
70
|
+
define_singleton_method(method_name) do |*args, &blk|
|
71
|
+
target_obj.send(method_name, *args, &blk)
|
72
|
+
end
|
73
|
+
|
74
|
+
if target_obj.singleton_class.public_method_defined?(method_name)
|
75
|
+
# no-op, it's already public
|
76
|
+
elsif target_obj.singleton_class.protected_method_defined?(method_name)
|
77
|
+
singleton_class.send(:protected, method_name)
|
78
|
+
elsif target_obj.singleton_class.private_method_defined?(method_name)
|
79
|
+
singleton_class.send(:private, method_name)
|
80
|
+
else
|
81
|
+
raise "This should never happen. Report to #dev-productivity"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def kind_of?(other) # rubocop:disable PrisonGuard/BanBuiltinMethodOverride
|
87
|
+
is_a?(other)
|
88
|
+
end
|
89
|
+
|
90
|
+
def is_a?(other) # rubocop:disable PrisonGuard/BanBuiltinMethodOverride
|
91
|
+
if !other.is_a?(Module)
|
92
|
+
raise TypeError.new("class or module required")
|
93
|
+
end
|
94
|
+
|
95
|
+
# This makes is_a? return true for T::InterfaceWrapper (and its ancestors),
|
96
|
+
# as well as for @interface_mod and its ancestors.
|
97
|
+
self.class <= other || @interface_mod <= other
|
98
|
+
end
|
99
|
+
|
100
|
+
# Prefixed because we're polluting the namespace of the interface we're wrapping, and we don't
|
101
|
+
# want anyone else (besides dynamic_cast) calling it.
|
102
|
+
def __target_obj_DO_NOT_USE
|
103
|
+
@target_obj
|
104
|
+
end
|
105
|
+
|
106
|
+
# Prefixed because we're polluting the namespace of the interface we're wrapping, and we don't
|
107
|
+
# want anyone else (besides wrapped_dynamic_cast) calling it.
|
108
|
+
def __interface_mod_DO_NOT_USE
|
109
|
+
@interface_mod
|
110
|
+
end
|
111
|
+
|
112
|
+
# "Cast" an object to another type. If `obj` is an InterfaceWrapper, returns the the wrapped
|
113
|
+
# object if that matches `type`. Otherwise, returns `obj` if it matches `type`. Otherwise,
|
114
|
+
# returns nil.
|
115
|
+
#
|
116
|
+
# @param obj [Object] object to cast
|
117
|
+
# @param mod [Module] type to cast `obj` to
|
118
|
+
#
|
119
|
+
# @example
|
120
|
+
# if (impl = T::InterfaceWrapper.dynamic_cast(iface, MyImplementation))
|
121
|
+
# impl.do_things
|
122
|
+
# end
|
123
|
+
def self.dynamic_cast(obj, mod)
|
124
|
+
if obj.is_a?(T::InterfaceWrapper)
|
125
|
+
target_obj = obj.__target_obj_DO_NOT_USE
|
126
|
+
target_obj.is_a?(mod) ? target_obj : nil
|
127
|
+
elsif obj.is_a?(mod)
|
128
|
+
obj
|
129
|
+
else
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Like dynamic_cast, but puts the result in its own wrapper if necessary.
|
135
|
+
#
|
136
|
+
# @param obj [Object] object to cast
|
137
|
+
# @param mod [Module] type to cast `obj` to
|
138
|
+
def self.wrapped_dynamic_cast(obj, mod)
|
139
|
+
# Avoid unwrapping and creating an equivalent wrapper.
|
140
|
+
if obj.is_a?(T::InterfaceWrapper) && obj.__interface_mod_DO_NOT_USE == mod
|
141
|
+
return obj
|
142
|
+
end
|
143
|
+
|
144
|
+
cast_obj = dynamic_cast(obj, mod)
|
145
|
+
if cast_obj.nil?
|
146
|
+
nil
|
147
|
+
elsif cast_obj.class == mod
|
148
|
+
# Nothing to wrap, they want the full class
|
149
|
+
cast_obj
|
150
|
+
else
|
151
|
+
new(cast_obj, mod)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.self_methods
|
156
|
+
@self_methods ||= self.instance_methods(false).to_set
|
157
|
+
end
|
158
|
+
end
|