Class: T::Enum

Inherits:
Object
  • Object
show all
Defined in:
lib/sorbet/eraser/t/enum.rb

Overview

This is mostly copy-pasted from sorbet-runtime since we have to maintain the same behavior.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(serialized_val = nil) ⇒ Enum

Private implementation ##



146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/sorbet/eraser/t/enum.rb', line 146

def initialize(serialized_val=nil)
  raise 'T::Enum is abstract' if self.class == T::Enum
  if !self.class.started_initializing?
    raise "Must instantiate all enum values of #{self.class} inside 'enums do'."
  end
  if self.class.fully_initialized?
    raise "Cannot instantiate a new enum value of #{self.class} after it has been initialized."
  end

  serialized_val = serialized_val.frozen? ? serialized_val : serialized_val.dup.freeze
  @serialized_val = serialized_val
  @const_name = nil
  self.class._register_instance(self)
end

Class Method Details

._load(args) ⇒ Object



248
249
250
# File 'lib/sorbet/eraser/t/enum.rb', line 248

def self._load(args)
  deserialize(Marshal.load(args))
end

._register_instance(instance) ⇒ Object

Maintains the order in which values are defined



190
191
192
193
# File 'lib/sorbet/eraser/t/enum.rb', line 190

def self._register_instance(instance)
  @values ||= []
  @values << instance
end

.deserialize(mongo_value) ⇒ Object

Note: Failed CriticalMethodsNoRuntimeTypingTest



71
72
73
74
75
76
# File 'lib/sorbet/eraser/t/enum.rb', line 71

def self.deserialize(mongo_value)
  if self == T::Enum
    raise "Cannot call T::Enum.deserialize directly. You must call on a specific child class."
  end
  self.from_serialized(mongo_value)
end

.each_value(&blk) ⇒ Object

This exists for compatibility with the interface of Hash & mostly to support the HashEachMethods Rubocop.



19
20
21
22
23
24
25
# File 'lib/sorbet/eraser/t/enum.rb', line 19

def self.each_value(&blk)
  if blk
    values.each(&blk)
  else
    values.each
  end
end

.enums(&blk) ⇒ Object

Entrypoint for allowing people to register new enum values. All enum values must be defined within this block.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/sorbet/eraser/t/enum.rb', line 197

def self.enums(&blk)
  raise "enums cannot be defined for T::Enum" if self == T::Enum
  raise "Enum #{self} was already initialized" if @fully_initialized
  raise "Enum #{self} is still initializing" if @started_initializing

  @started_initializing = true

  @values = nil

  yield

  @mapping = nil
  @mapping = {}

  # Freeze the Enum class and bind the constant names into each of the instances.
  self.constants(false).each do |const_name|
    instance = self.const_get(const_name, false)
    if !instance.is_a?(self)
      raise "Invalid constant #{self}::#{const_name} on enum. " \
        "All constants defined for an enum must be instances itself (e.g. `Foo = new`)."
    end

    instance._bind_name(const_name)
    serialized = instance.serialize
    if @mapping.include?(serialized)
      raise "Enum values must have unique serializations. Value '#{serialized}' is repeated on #{self}."
    end
    @mapping[serialized] = instance
  end
  @values.freeze
  @mapping.freeze

  orphaned_instances = @values - @mapping.values
  if !orphaned_instances.empty?
    raise "Enum values must be assigned to constants: #{orphaned_instances.map {|v| v.instance_variable_get('@serialized_val')}}"
  end

  @fully_initialized = true
end

.from_serialized(serialized_val) ⇒ self

Convert from serialized value to enum instance.

Returns:

  • (self)

Raises:

  • (KeyError)

    if serialized value does not match any instance.



40
41
42
43
44
45
46
# File 'lib/sorbet/eraser/t/enum.rb', line 40

def self.from_serialized(serialized_val)
  res = try_deserialize(serialized_val)
  if res.nil?
    raise KeyError.new("Enum #{self} key not found: #{serialized_val.inspect}")
  end
  res
end

.fully_initialized?Boolean

Returns:

  • (Boolean)


185
186
187
# File 'lib/sorbet/eraser/t/enum.rb', line 185

def self.fully_initialized?
  @fully_initialized ||= false
end

.has_serialized?(serialized_val) ⇒ Boolean

Note: It would have been nice to make this method final before people started overriding it.

Returns:

  • (Boolean)

    Does the given serialized value correspond with any of this enum's values.



50
51
52
53
54
55
56
# File 'lib/sorbet/eraser/t/enum.rb', line 50

def self.has_serialized?(serialized_val)
  if @mapping.nil?
    raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
      " Enums are not initialized until the 'enums do' block they are defined in has finished running."
  end
  @mapping.include?(serialized_val)
end

.inherited(child_class) ⇒ Object



237
238
239
240
241
# File 'lib/sorbet/eraser/t/enum.rb', line 237

def self.inherited(child_class)
  super

  raise "Inheriting from children of T::Enum is prohibited" if self != T::Enum
end

.serialize(instance) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
# File 'lib/sorbet/eraser/t/enum.rb', line 58

def self.serialize(instance)
  return nil if instance.nil?

  if self == T::Enum
    raise "Cannot call T::Enum.serialize directly. You must call on a specific child class."
  end
  if instance.class != self
    raise "Cannot call #serialize on a value that is not an instance of #{self}."
  end
  instance.serialize
end

.started_initializing?Boolean

Returns:

  • (Boolean)


181
182
183
# File 'lib/sorbet/eraser/t/enum.rb', line 181

def self.started_initializing?
  @started_initializing ||= false
end

.try_deserialize(serialized_val) ⇒ Object

Convert from serialized value to enum instance



28
29
30
31
32
33
34
# File 'lib/sorbet/eraser/t/enum.rb', line 28

def self.try_deserialize(serialized_val)
  if @mapping.nil?
    raise "Attempting to access serialization map of #{self.class} before it has been initialized." \
      " Enums are not initialized until the 'enums do' block they are defined in has finished running."
  end
  @mapping[serialized_val]
end

.valuesObject

Enum class methods ##



9
10
11
12
13
14
15
# File 'lib/sorbet/eraser/t/enum.rb', line 9

def self.values
  if @values.nil?
    raise "Attempting to access values of #{self.class} before it has been initialized." \
      " Enums are not initialized until the 'enums do' block they are defined in has finished running."
  end
  @values
end

Instance Method Details

#<=>(other) ⇒ Object



105
106
107
108
109
110
111
112
# File 'lib/sorbet/eraser/t/enum.rb', line 105

def <=>(other)
  case other
  when self.class
    self.serialize <=> other.serialize
  else
    nil
  end
end

#==(other) ⇒ Object



126
127
128
129
130
131
132
133
# File 'lib/sorbet/eraser/t/enum.rb', line 126

def ==(other)
  case other
  when String
    false
  else
    super(other)
  end
end

#===(other) ⇒ Object



135
136
137
138
139
140
141
142
# File 'lib/sorbet/eraser/t/enum.rb', line 135

def ===(other)
  case other
  when String
    false
  else
    super(other)
  end
end

#_bind_name(const_name) ⇒ Object



168
169
170
171
172
# File 'lib/sorbet/eraser/t/enum.rb', line 168

def _bind_name(const_name)
  @const_name = const_name
  @serialized_val = const_to_serialized_val(const_name) if @serialized_val.nil?
  freeze
end

#_dump(_level) ⇒ Object

Marshal support



244
245
246
# File 'lib/sorbet/eraser/t/enum.rb', line 244

def _dump(_level)
  Marshal.dump(serialize)
end

#cloneObject



84
85
86
# File 'lib/sorbet/eraser/t/enum.rb', line 84

def clone
  self
end

#dupObject

Enum instance methods ##



80
81
82
# File 'lib/sorbet/eraser/t/enum.rb', line 80

def dup
  self
end

#inspectObject



101
102
103
# File 'lib/sorbet/eraser/t/enum.rb', line 101

def inspect
  "#<#{self.class.name}::#{@const_name || '__UNINITIALIZED__'}>"
end

#serializeObject



88
89
90
91
# File 'lib/sorbet/eraser/t/enum.rb', line 88

def serialize
  assert_bound!
  @serialized_val
end

#to_json(*args) ⇒ Object



93
94
95
# File 'lib/sorbet/eraser/t/enum.rb', line 93

def to_json(*args)
  serialize.to_json(*args)
end

#to_sObject



97
98
99
# File 'lib/sorbet/eraser/t/enum.rb', line 97

def to_s
  inspect
end

#to_strObject

NB: Do not call this method. This exists to allow for a safe migration path in places where enum values are compared directly against string values.

Ruby's string has a weird quirk where 'my_string' == obj calls obj.==('my_string') if obj responds to the to_str method. It does not actually call to_str however.

See https://ruby-doc.org/core-2.4.0/String.html#method-i-3D-3D

Raises:

  • (NoMethodError)


121
122
123
124
# File 'lib/sorbet/eraser/t/enum.rb', line 121

def to_str
  msg = 'Implicit conversion of Enum instances to strings is not allowed. Call #serialize instead.'
  raise NoMethodError.new(msg)
end