sorbet-runtime 0.5.5841
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 +7 -0
- data/lib/sorbet-runtime.rb +116 -0
- data/lib/types/_types.rb +285 -0
- data/lib/types/abstract_utils.rb +50 -0
- data/lib/types/boolean.rb +8 -0
- data/lib/types/compatibility_patches.rb +95 -0
- data/lib/types/configuration.rb +428 -0
- data/lib/types/enum.rb +349 -0
- data/lib/types/generic.rb +23 -0
- data/lib/types/helpers.rb +39 -0
- data/lib/types/interface_wrapper.rb +158 -0
- data/lib/types/non_forcing_constants.rb +51 -0
- data/lib/types/private/abstract/data.rb +36 -0
- data/lib/types/private/abstract/declare.rb +48 -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 +111 -0
- data/lib/types/private/decl_state.rb +30 -0
- data/lib/types/private/final.rb +51 -0
- data/lib/types/private/methods/_methods.rb +460 -0
- data/lib/types/private/methods/call_validation.rb +1149 -0
- data/lib/types/private/methods/decl_builder.rb +228 -0
- data/lib/types/private/methods/modes.rb +16 -0
- data/lib/types/private/methods/signature.rb +196 -0
- data/lib/types/private/methods/signature_validation.rb +229 -0
- data/lib/types/private/mixins/mixins.rb +27 -0
- data/lib/types/private/retry.rb +10 -0
- data/lib/types/private/runtime_levels.rb +56 -0
- data/lib/types/private/sealed.rb +65 -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/type_alias.rb +26 -0
- data/lib/types/private/types/void.rb +34 -0
- data/lib/types/profile.rb +31 -0
- data/lib/types/props/_props.rb +161 -0
- data/lib/types/props/constructor.rb +40 -0
- data/lib/types/props/custom_type.rb +108 -0
- data/lib/types/props/decorator.rb +672 -0
- data/lib/types/props/errors.rb +8 -0
- data/lib/types/props/generated_code_validation.rb +268 -0
- data/lib/types/props/has_lazily_specialized_methods.rb +92 -0
- data/lib/types/props/optional.rb +81 -0
- data/lib/types/props/plugin.rb +37 -0
- data/lib/types/props/pretty_printable.rb +107 -0
- data/lib/types/props/private/apply_default.rb +170 -0
- data/lib/types/props/private/deserializer_generator.rb +165 -0
- data/lib/types/props/private/parser.rb +32 -0
- data/lib/types/props/private/serde_transform.rb +192 -0
- data/lib/types/props/private/serializer_generator.rb +77 -0
- data/lib/types/props/private/setter_factory.rb +134 -0
- data/lib/types/props/serializable.rb +330 -0
- data/lib/types/props/type_validation.rb +111 -0
- data/lib/types/props/utils.rb +59 -0
- data/lib/types/props/weak_constructor.rb +67 -0
- data/lib/types/runtime_profiled.rb +24 -0
- data/lib/types/sig.rb +30 -0
- data/lib/types/struct.rb +18 -0
- data/lib/types/types/attached_class.rb +37 -0
- data/lib/types/types/base.rb +151 -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 +37 -0
- data/lib/types/types/noreturn.rb +29 -0
- data/lib/types/types/proc.rb +51 -0
- data/lib/types/types/self_type.rb +35 -0
- data/lib/types/types/simple.rb +33 -0
- data/lib/types/types/t_enum.rb +38 -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 +34 -0
- data/lib/types/types/typed_enumerable.rb +161 -0
- data/lib/types/types/typed_enumerator.rb +36 -0
- data/lib/types/types/typed_hash.rb +43 -0
- data/lib/types/types/typed_range.rb +26 -0
- data/lib/types/types/typed_set.rb +36 -0
- data/lib/types/types/union.rb +56 -0
- data/lib/types/types/untyped.rb +29 -0
- data/lib/types/utils.rb +217 -0
- metadata +223 -0
data/lib/types/enum.rb
ADDED
@@ -0,0 +1,349 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
# Enumerations allow for type-safe declarations of a fixed set of values.
|
5
|
+
#
|
6
|
+
# Every value is a singleton instance of the class (i.e. `Suit::SPADE.is_a?(Suit) == true`).
|
7
|
+
#
|
8
|
+
# Each value has a corresponding serialized value. By default this is the constant's name converted
|
9
|
+
# to lowercase (e.g. `Suit::Club.serialize == 'club'`); however a custom value may be passed to the
|
10
|
+
# constructor. Enum will `freeze` the serialized value.
|
11
|
+
#
|
12
|
+
# @example Declaring an Enum:
|
13
|
+
# class Suit < T::Enum
|
14
|
+
# enums do
|
15
|
+
# CLUB = new
|
16
|
+
# SPADE = new
|
17
|
+
# DIAMOND = new
|
18
|
+
# HEART = new
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# @example Custom serialization value:
|
23
|
+
# class Status < T::Enum
|
24
|
+
# enums do
|
25
|
+
# READY = new('rdy')
|
26
|
+
# ...
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# @example Accessing values:
|
31
|
+
# Suit::SPADE
|
32
|
+
#
|
33
|
+
# @example Converting from serialized value to enum instance:
|
34
|
+
# Suit.deserialize('club') == Suit::CLUB
|
35
|
+
#
|
36
|
+
# @example Using enums in type signatures:
|
37
|
+
# sig {params(suit: Suit).returns(Boolean)}
|
38
|
+
# def is_red?(suit); ...; end
|
39
|
+
#
|
40
|
+
# WARNING: Enum instances are singletons that are shared among all their users. Their internals
|
41
|
+
# should be kept immutable to avoid unpredictable action at a distance.
|
42
|
+
class T::Enum
|
43
|
+
extend T::Sig
|
44
|
+
extend T::Props::CustomType
|
45
|
+
|
46
|
+
# TODO(jez) Might want to restrict this, or make subclasses provide this type
|
47
|
+
SerializedVal = T.type_alias {T.untyped}
|
48
|
+
private_constant :SerializedVal
|
49
|
+
|
50
|
+
## Enum class methods ##
|
51
|
+
sig {returns(T::Array[T.attached_class])}
|
52
|
+
def self.values
|
53
|
+
if @values.nil?
|
54
|
+
raise "Attempting to access values of #{self.class} before it has been initialized." \
|
55
|
+
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
56
|
+
end
|
57
|
+
@values
|
58
|
+
end
|
59
|
+
|
60
|
+
# Convert from serialized value to enum instance
|
61
|
+
#
|
62
|
+
# Note: It would have been nice to make this method final before people started overriding it.
|
63
|
+
# Note: Failed CriticalMethodsNoRuntimeTypingTest
|
64
|
+
sig {params(serialized_val: SerializedVal).returns(T.nilable(T.attached_class)).checked(:never)}
|
65
|
+
def self.try_deserialize(serialized_val)
|
66
|
+
if @mapping.nil?
|
67
|
+
raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
|
68
|
+
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
69
|
+
end
|
70
|
+
@mapping[serialized_val]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Convert from serialized value to enum instance.
|
74
|
+
#
|
75
|
+
# Note: It would have been nice to make this method final before people started overriding it.
|
76
|
+
# Note: Failed CriticalMethodsNoRuntimeTypingTest
|
77
|
+
#
|
78
|
+
# @return [self]
|
79
|
+
# @raise [KeyError] if serialized value does not match any instance.
|
80
|
+
sig {overridable.params(serialized_val: SerializedVal).returns(T.attached_class).checked(:never)}
|
81
|
+
def self.from_serialized(serialized_val)
|
82
|
+
res = try_deserialize(serialized_val)
|
83
|
+
if res.nil?
|
84
|
+
raise KeyError.new("Enum #{self} key not found: #{serialized_val.inspect}")
|
85
|
+
end
|
86
|
+
res
|
87
|
+
end
|
88
|
+
|
89
|
+
# Note: It would have been nice to make this method final before people started overriding it.
|
90
|
+
# @return [Boolean] Does the given serialized value correspond with any of this enum's values.
|
91
|
+
sig {overridable.params(serialized_val: SerializedVal).returns(T::Boolean)}
|
92
|
+
def self.has_serialized?(serialized_val)
|
93
|
+
if @mapping.nil?
|
94
|
+
raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
|
95
|
+
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
96
|
+
end
|
97
|
+
@mapping.include?(serialized_val)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Note: Failed CriticalMethodsNoRuntimeTypingTest
|
101
|
+
sig {override.params(instance: T.nilable(T::Enum)).returns(SerializedVal).checked(:never)}
|
102
|
+
def self.serialize(instance)
|
103
|
+
# This is needed otherwise if a Chalk::ODM::Document with a property of the shape
|
104
|
+
# T::Hash[T.nilable(MyEnum), Integer] and a value that looks like {nil => 0} is
|
105
|
+
# serialized, we throw the error on L102.
|
106
|
+
return nil if instance.nil?
|
107
|
+
|
108
|
+
if self == T::Enum
|
109
|
+
raise "Cannot call T::Enum.serialize directly. You must call on a specific child class."
|
110
|
+
end
|
111
|
+
if instance.class != self
|
112
|
+
raise "Cannot call #serialize on a value that is not an instance of #{self}."
|
113
|
+
end
|
114
|
+
instance.serialize
|
115
|
+
end
|
116
|
+
|
117
|
+
# Note: Failed CriticalMethodsNoRuntimeTypingTest
|
118
|
+
sig {override.params(mongo_value: SerializedVal).returns(T.attached_class).checked(:never)}
|
119
|
+
def self.deserialize(mongo_value)
|
120
|
+
if self == T::Enum
|
121
|
+
raise "Cannot call T::Enum.deserialize directly. You must call on a specific child class."
|
122
|
+
end
|
123
|
+
self.from_serialized(mongo_value)
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
## Enum instance methods ##
|
128
|
+
|
129
|
+
|
130
|
+
sig {returns(T.self_type)}
|
131
|
+
def dup
|
132
|
+
self
|
133
|
+
end
|
134
|
+
|
135
|
+
sig {returns(T.self_type).checked(:tests)}
|
136
|
+
def clone
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
# Note: Failed CriticalMethodsNoRuntimeTypingTest
|
141
|
+
sig {returns(SerializedVal).checked(:never)}
|
142
|
+
def serialize
|
143
|
+
assert_bound!
|
144
|
+
@serialized_val
|
145
|
+
end
|
146
|
+
|
147
|
+
sig {params(args: T.untyped).returns(T.untyped)}
|
148
|
+
def to_json(*args)
|
149
|
+
serialize.to_json(*args)
|
150
|
+
end
|
151
|
+
|
152
|
+
sig {returns(String)}
|
153
|
+
def to_s
|
154
|
+
inspect
|
155
|
+
end
|
156
|
+
|
157
|
+
sig {returns(String)}
|
158
|
+
def inspect
|
159
|
+
"#<#{self.class.name}::#{@const_name || '__UNINITIALIZED__'}>"
|
160
|
+
end
|
161
|
+
|
162
|
+
sig {params(other: BasicObject).returns(T.nilable(Integer))}
|
163
|
+
def <=>(other)
|
164
|
+
case other
|
165
|
+
when self.class
|
166
|
+
self.serialize <=> other.serialize
|
167
|
+
else
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
# NB: Do not call this method. This exists to allow for a safe migration path in places where enum
|
174
|
+
# values are compared directly against string values.
|
175
|
+
#
|
176
|
+
# Ruby's string has a weird quirk where `'my_string' == obj` calls obj.==('my_string') if obj
|
177
|
+
# responds to the `to_str` method. It does not actually call `to_str` however.
|
178
|
+
#
|
179
|
+
# See https://ruby-doc.org/core-2.4.0/String.html#method-i-3D-3D
|
180
|
+
sig {returns(String)}
|
181
|
+
def to_str
|
182
|
+
msg = 'Implicit conversion of Enum instances to strings is not allowed. Call #serialize instead.'
|
183
|
+
if T::Configuration.legacy_t_enum_migration_mode?
|
184
|
+
T::Configuration.soft_assert_handler(
|
185
|
+
msg,
|
186
|
+
storytime: {class: self.class.name},
|
187
|
+
)
|
188
|
+
serialize.to_s
|
189
|
+
else
|
190
|
+
raise NoMethodError.new(msg)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
sig {params(other: BasicObject).returns(T::Boolean).checked(:never)}
|
195
|
+
def ==(other)
|
196
|
+
case other
|
197
|
+
when String
|
198
|
+
if T::Configuration.legacy_t_enum_migration_mode?
|
199
|
+
comparison_assertion_failed(:==, other)
|
200
|
+
self.serialize == other
|
201
|
+
else
|
202
|
+
false
|
203
|
+
end
|
204
|
+
else
|
205
|
+
super(other)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
sig {params(other: BasicObject).returns(T::Boolean).checked(:never)}
|
210
|
+
def ===(other)
|
211
|
+
case other
|
212
|
+
when String
|
213
|
+
if T::Configuration.legacy_t_enum_migration_mode?
|
214
|
+
comparison_assertion_failed(:===, other)
|
215
|
+
self.serialize == other
|
216
|
+
else
|
217
|
+
false
|
218
|
+
end
|
219
|
+
else
|
220
|
+
super(other)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
sig {params(method: Symbol, other: T.untyped).void}
|
225
|
+
private def comparison_assertion_failed(method, other)
|
226
|
+
T::Configuration.soft_assert_handler(
|
227
|
+
'Enum to string comparison not allowed. Compare to the Enum instance directly instead. See go/enum-migration',
|
228
|
+
storytime: {
|
229
|
+
class: self.class.name,
|
230
|
+
self: self.inspect,
|
231
|
+
other: other,
|
232
|
+
other_class: other.class.name,
|
233
|
+
method: method,
|
234
|
+
}
|
235
|
+
)
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
## Private implementation ##
|
240
|
+
|
241
|
+
|
242
|
+
sig {params(serialized_val: SerializedVal).void}
|
243
|
+
private def initialize(serialized_val=nil)
|
244
|
+
raise 'T::Enum is abstract' if self.class == T::Enum
|
245
|
+
if !self.class.started_initializing?
|
246
|
+
raise "Must instantiate all enum values of #{self.class} inside 'enums do'."
|
247
|
+
end
|
248
|
+
if self.class.fully_initialized?
|
249
|
+
raise "Cannot instantiate a new enum value of #{self.class} after it has been initialized."
|
250
|
+
end
|
251
|
+
|
252
|
+
serialized_val = serialized_val.frozen? ? serialized_val : serialized_val.dup.freeze
|
253
|
+
@serialized_val = T.let(serialized_val, T.nilable(SerializedVal))
|
254
|
+
@const_name = T.let(nil, T.nilable(Symbol))
|
255
|
+
self.class._register_instance(self)
|
256
|
+
end
|
257
|
+
|
258
|
+
sig {returns(NilClass).checked(:never)}
|
259
|
+
private def assert_bound!
|
260
|
+
if @const_name.nil?
|
261
|
+
raise "Attempting to access Enum value on #{self.class} before it has been initialized." \
|
262
|
+
" Enums are not initialized until the 'enums do' block they are defined in has finished running."
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
sig {params(const_name: Symbol).void}
|
267
|
+
def _bind_name(const_name)
|
268
|
+
@const_name = const_name
|
269
|
+
@serialized_val = const_to_serialized_val(const_name) if @serialized_val.nil?
|
270
|
+
freeze
|
271
|
+
end
|
272
|
+
|
273
|
+
sig {params(const_name: Symbol).returns(String)}
|
274
|
+
private def const_to_serialized_val(const_name)
|
275
|
+
# Historical note: We convert to lowercase names because the majority of existing calls to
|
276
|
+
# `make_accessible` were arrays of lowercase strings. Doing this conversion allowed for the
|
277
|
+
# least amount of repetition in migrated declarations.
|
278
|
+
const_name.to_s.downcase.freeze
|
279
|
+
end
|
280
|
+
|
281
|
+
sig {returns(T::Boolean)}
|
282
|
+
def self.started_initializing?
|
283
|
+
@started_initializing = T.let(@started_initializing, T.nilable(T::Boolean))
|
284
|
+
@started_initializing ||= false
|
285
|
+
end
|
286
|
+
|
287
|
+
sig {returns(T::Boolean)}
|
288
|
+
def self.fully_initialized?
|
289
|
+
@fully_initialized = T.let(@fully_initialized, T.nilable(T::Boolean))
|
290
|
+
@fully_initialized ||= false
|
291
|
+
end
|
292
|
+
|
293
|
+
# Maintains the order in which values are defined
|
294
|
+
sig {params(instance: T.untyped).void}
|
295
|
+
def self._register_instance(instance)
|
296
|
+
@values ||= []
|
297
|
+
@values << T.cast(instance, T.attached_class)
|
298
|
+
end
|
299
|
+
|
300
|
+
# Entrypoint for allowing people to register new enum values.
|
301
|
+
# All enum values must be defined within this block.
|
302
|
+
sig {params(blk: T.proc.void).void}
|
303
|
+
def self.enums(&blk)
|
304
|
+
raise "enums cannot be defined for T::Enum" if self == T::Enum
|
305
|
+
raise "Enum #{self} was already initialized" if @fully_initialized
|
306
|
+
raise "Enum #{self} is still initializing" if @started_initializing
|
307
|
+
|
308
|
+
@started_initializing = true
|
309
|
+
|
310
|
+
@values = T.let(nil, T.nilable(T::Array[T.attached_class]))
|
311
|
+
|
312
|
+
yield
|
313
|
+
|
314
|
+
@mapping = T.let(nil, T.nilable(T::Hash[SerializedVal, T.attached_class]))
|
315
|
+
@mapping = {}
|
316
|
+
|
317
|
+
# Freeze the Enum class and bind the constant names into each of the instances.
|
318
|
+
self.constants(false).each do |const_name|
|
319
|
+
instance = self.const_get(const_name, false)
|
320
|
+
if !instance.is_a?(self)
|
321
|
+
raise "Invalid constant #{self}::#{const_name} on enum. " \
|
322
|
+
"All constants defined for an enum must be instances itself (e.g. `Foo = new`)."
|
323
|
+
end
|
324
|
+
|
325
|
+
instance._bind_name(const_name)
|
326
|
+
serialized = instance.serialize
|
327
|
+
if @mapping.include?(serialized)
|
328
|
+
raise "Enum values must have unique serializations. Value '#{serialized}' is repeated on #{self}."
|
329
|
+
end
|
330
|
+
@mapping[serialized] = instance
|
331
|
+
end
|
332
|
+
@values.freeze
|
333
|
+
@mapping.freeze
|
334
|
+
|
335
|
+
orphaned_instances = T.must(@values) - @mapping.values
|
336
|
+
if !orphaned_instances.empty?
|
337
|
+
raise "Enum values must be assigned to constants: #{orphaned_instances.map {|v| v.instance_variable_get('@serialized_val')}}"
|
338
|
+
end
|
339
|
+
|
340
|
+
@fully_initialized = true
|
341
|
+
end
|
342
|
+
|
343
|
+
sig {params(child_class: Module).void}
|
344
|
+
def self.inherited(child_class)
|
345
|
+
super
|
346
|
+
|
347
|
+
raise "Inheriting from children of T::Enum is prohibited" if self != T::Enum
|
348
|
+
end
|
349
|
+
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, lower: T.untyped, upper: BasicObject)
|
17
|
+
T::Types::TypeMember.new(variance) # rubocop:disable PrisonGuard/UseOpusTypesShortcut
|
18
|
+
end
|
19
|
+
|
20
|
+
def type_template(variance=:invariant, fixed: nil, lower: T.untyped, upper: BasicObject)
|
21
|
+
T::Types::TypeTemplate.new(variance) # rubocop:disable PrisonGuard/UseOpusTypesShortcut
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,39 @@
|
|
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
|
+
def final!
|
20
|
+
Private::Final.declare(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def sealed!
|
24
|
+
Private::Sealed.declare(self, Kernel.caller(1..1)&.first&.split(':')&.first)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Causes a mixin to also mix in class methods from the named module.
|
28
|
+
#
|
29
|
+
# Nearly equivalent to
|
30
|
+
#
|
31
|
+
# def self.included(other)
|
32
|
+
# other.extend(mod)
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# Except that it is statically analyzed by sorbet.
|
36
|
+
def mixes_in_class_methods(mod)
|
37
|
+
Private::Mixins.declare_mixes_in_class_methods(self, mod)
|
38
|
+
end
|
39
|
+
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
|