Class: T::Enum

Inherits:
Object
  • Object
show all
Extended by:
Props::CustomType, Sig
Defined in:
lib/types/enum.rb

Overview

Enumerations allow for type-safe declarations of a fixed set of values.

Every value is a singleton instance of the class (i.e. ‘Suit::SPADE.is_a?(Suit) == true`).

Each value has a corresponding serialized value. By default this is the constant’s name converted to lowercase (e.g. ‘Suit::Club.serialize == ’club’‘); however a custom value may be passed to the constructor. Enum will `freeze` the serialized value.

WARNING: Enum instances are singletons that are shared among all their users. Their internals should be kept immutable to avoid unpredictable action at a distance.

Examples:

Declaring an Enum:

class Suit < T::Enum
  enums do
    CLUB = new
    SPADE = new
    DIAMOND = new
    HEART = new
  end
end

Custom serialization value:

class Status < T::Enum
  enums do
    READY = new('rdy')
    ...
  end
end

Accessing values:

Suit::SPADE

Converting from serialized value to enum instance:

Suit.deserialize('club') == Suit::CLUB

Using enums in type signatures:

sig {params(suit: Suit).returns(Boolean)}
def is_red?(suit); ...; end

Defined Under Namespace

Modules: LegacyMigrationMode

Constant Summary

Constants included from Helpers

Helpers::Private

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Sig

sig

Methods included from Private::Methods::SingletonMethodHooks

#singleton_method_added

Methods included from Private::Methods::MethodHooks

#method_added

Methods included from Props::CustomType

checked_serialize, deserialize, included, instance?, scalar_type?, valid?, valid_serialization?

Methods included from Helpers

#abstract!, #final!, #interface!, #mixes_in_class_methods, #requires_ancestor, #sealed!

Constructor Details

#initialize(serialized_val = UNSET) ⇒ Enum

Returns a new instance of Enum.



303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/types/enum.rb', line 303

def initialize(serialized_val=UNSET)
  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 = T.let(serialized_val, T.nilable(SerializedVal))
  @const_name = T.let(nil, T.nilable(Symbol))
  self.class._register_instance(self)
end

Class Method Details

._load(args) ⇒ Object



425
426
427
# File 'lib/types/enum.rb', line 425

def self._load(args)
  deserialize(Marshal.load(args)) # rubocop:disable Security/MarshalLoad
end

._register_instance(instance) ⇒ Object



358
359
360
361
# File 'lib/types/enum.rb', line 358

def self._register_instance(instance)
  @values ||= []
  @values << T.cast(instance, T.attached_class)
end

.deserialize(mongo_value) ⇒ Object



132
133
134
135
136
137
# File 'lib/types/enum.rb', line 132

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



67
68
69
70
71
72
73
# File 'lib/types/enum.rb', line 67

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

.enums(&blk) ⇒ Object



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/types/enum.rb', line 366

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 = T.let(nil, T.nilable(T::Array[T.attached_class]))

  yield

  @mapping = T.let(nil, T.nilable(T::Hash[SerializedVal, T.attached_class]))
  @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 = T.must(@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) ⇒ Object



95
96
97
98
99
100
101
# File 'lib/types/enum.rb', line 95

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:



349
350
351
352
353
354
# File 'lib/types/enum.rb', line 349

def self.fully_initialized?
  unless defined?(@fully_initialized)
    @fully_initialized = T.let(false, T.nilable(T::Boolean))
  end
  T.must(@fully_initialized)
end

.has_serialized?(serialized_val) ⇒ Boolean

Returns:



106
107
108
109
110
111
# File 'lib/types/enum.rb', line 106

def self.has_serialized?(serialized_val)
  if @mapping.nil?
    raise(UNBOUND_SERIALIZATION_MAP_MESSAGE % self.class)
  end
  @mapping.include?(serialized_val)
end

.inherited(child_class) ⇒ Object



407
408
409
410
411
412
413
414
415
416
# File 'lib/types/enum.rb', line 407

def self.inherited(child_class)
  super

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

  # "oj" gem JSON support
  if Object.const_defined?(:Oj)
    Object.const_get(:Oj).register_odd(child_class, child_class, :try_deserialize, :serialize)
  end
end

.serialize(instance) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/types/enum.rb', line 115

def self.serialize(instance)
  # This is needed otherwise if a Chalk::ODM::Document with a property of the shape
  # T::Hash[T.nilable(MyEnum), Integer] and a value that looks like {nil => 0} is
  # serialized, we throw the error on L102.
  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:



341
342
343
344
345
346
# File 'lib/types/enum.rb', line 341

def self.started_initializing?
  unless defined?(@started_initializing)
    @started_initializing = T.let(false, T.nilable(T::Boolean))
  end
  T.must(@started_initializing)
end

.try_deserialize(serialized_val) ⇒ Object



80
81
82
83
84
85
# File 'lib/types/enum.rb', line 80

def self.try_deserialize(serialized_val)
  if @mapping.nil?
    raise(UNBOUND_SERIALIZATION_MAP_MESSAGE % self.class)
  end
  @mapping[serialized_val]
end

.valuesObject



57
58
59
60
61
62
# File 'lib/types/enum.rb', line 57

def self.values
  if @values.nil?
    raise(UNBOUND_VALUES_MESSAGE % self.class)
  end
  @values
end

Instance Method Details

#<=>(other) ⇒ Object



202
203
204
205
206
207
208
209
# File 'lib/types/enum.rb', line 202

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

#_bind_name(const_name) ⇒ Object



326
327
328
329
330
# File 'lib/types/enum.rb', line 326

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

#_dump(_level) ⇒ Object



420
421
422
# File 'lib/types/enum.rb', line 420

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

#as_json(*args) ⇒ Object



182
183
184
185
186
# File 'lib/types/enum.rb', line 182

def as_json(*args)
  serialized_val = serialize
  return serialized_val unless serialized_val.respond_to?(:as_json)
  serialized_val.as_json(*args)
end

#cloneObject



147
148
149
# File 'lib/types/enum.rb', line 147

def clone
  self
end

#dupObject



142
143
144
# File 'lib/types/enum.rb', line 142

def dup
  self
end

#inspectObject



197
198
199
# File 'lib/types/enum.rb', line 197

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

#serializeObject



167
168
169
170
171
172
173
174
# File 'lib/types/enum.rb', line 167

def serialize
  # Same check and message as assert_bound!, open-coded to avoid the extra
  # method frame on this hot path.
  if @const_name.nil?
    raise(UNBOUND_VALUE_MESSAGE % self.class)
  end
  @serialized_val
end

#to_json(*args) ⇒ Object



177
178
179
# File 'lib/types/enum.rb', line 177

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

#to_sObject



192
193
194
# File 'lib/types/enum.rb', line 192

def to_s
  inspect
end

#to_strObject



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/types/enum.rb', line 219

def to_str
  msg = 'Implicit conversion of Enum instances to strings is not allowed. Call #serialize instead.'
  if T::Configuration.legacy_t_enum_migration_mode?
    T::Configuration.soft_assert_handler(
      msg,
      storytime: {
        class: self.class.name,
        caller_location: Kernel.caller_locations(1..1)&.[](0)&.then { "#{_1.path}:#{_1.lineno}" },
      },
    )
    serialize.to_s
  else
    Kernel.raise NoMethodError.new(msg)
  end
end