class T::Enum

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.

@example Declaring an Enum:

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

@example Custom serialization value:

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

@example Accessing values:

Suit::SPADE

@example Converting from serialized value to enum instance:

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

@example Using enums in type signatures:

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

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.

Constants

SerializedVal

TODO(jez) Might want to restrict this, or make subclasses provide this type

Public Class Methods

_load(args) click to toggle source
# File lib/types/enum.rb, line 363
def self._load(args)
  deserialize(Marshal.load(args)) # rubocop:disable Security/MarshalLoad
end
_register_instance(instance) click to toggle source
# File lib/types/enum.rb, line 301
def self._register_instance(instance)
  @values ||= []
  @values << T.cast(instance, T.attached_class)
end
deserialize(mongo_value) click to toggle source
# File lib/types/enum.rb, line 130
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) click to toggle source
# File lib/types/enum.rb, line 63
def self.each_value(&blk)
  if blk
    values.each(&blk)
  else
    values.each
  end
end
enums() { || ... } click to toggle source
# File lib/types/enum.rb, line 309
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) click to toggle source
# File lib/types/enum.rb, line 92
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?() click to toggle source
# File lib/types/enum.rb, line 294
def self.fully_initialized?
  @fully_initialized = T.let(@fully_initialized, T.nilable(T::Boolean))
  @fully_initialized ||= false
end
has_serialized?(serialized_val) click to toggle source
# File lib/types/enum.rb, line 103
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) click to toggle source
Calls superclass method
# File lib/types/enum.rb, line 350
def self.inherited(child_class)
  super

  raise "Inheriting from children of T::Enum is prohibited" if self != T::Enum
end
new(serialized_val=nil) click to toggle source
# File lib/types/enum.rb, line 249
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 = T.let(serialized_val, T.nilable(SerializedVal))
  @const_name = T.let(nil, T.nilable(Symbol))
  self.class._register_instance(self)
end
serialize(instance) click to toggle source
# File lib/types/enum.rb, line 113
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?() click to toggle source
# File lib/types/enum.rb, line 288
def self.started_initializing?
  @started_initializing = T.let(@started_initializing, T.nilable(T::Boolean))
  @started_initializing ||= false
end
try_deserialize(serialized_val) click to toggle source
# File lib/types/enum.rb, line 76
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
values() click to toggle source
# File lib/types/enum.rb, line 52
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

Public Instance Methods

<=>(other) click to toggle source
# File lib/types/enum.rb, line 172
def <=>(other)
  case other
  when self.class
    self.serialize <=> other.serialize
  else
    nil
  end
end
==(other) click to toggle source
Calls superclass method
# File lib/types/enum.rb, line 203
def ==(other)
  case other
  when String
    if T::Configuration.legacy_t_enum_migration_mode?
      comparison_assertion_failed(:==, other)
      self.serialize == other
    else
      false
    end
  else
    super(other)
  end
end
===(other) click to toggle source
Calls superclass method
# File lib/types/enum.rb, line 218
def ===(other)
  case other
  when String
    if T::Configuration.legacy_t_enum_migration_mode?
      comparison_assertion_failed(:===, other)
      self.serialize == other
    else
      false
    end
  else
    super(other)
  end
end
_bind_name(const_name) click to toggle source
# File lib/types/enum.rb, line 273
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) click to toggle source
# File lib/types/enum.rb, line 358
def _dump(_level)
  Marshal.dump(serialize)
end
clone() click to toggle source
# File lib/types/enum.rb, line 145
def clone
  self
end
dup() click to toggle source
# File lib/types/enum.rb, line 140
def dup
  self
end
inspect() click to toggle source
# File lib/types/enum.rb, line 167
def inspect
  "#<#{self.class.name}::#{@const_name || '__UNINITIALIZED__'}>"
end
serialize() click to toggle source
# File lib/types/enum.rb, line 151
def serialize
  assert_bound!
  @serialized_val
end
to_json(*args) click to toggle source
# File lib/types/enum.rb, line 157
def to_json(*args)
  serialize.to_json(*args)
end
to_s() click to toggle source
# File lib/types/enum.rb, line 162
def to_s
  inspect
end
to_str() click to toggle source
# File lib/types/enum.rb, line 189
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},
    )
    serialize.to_s
  else
    raise NoMethodError.new(msg)
  end
end

Private Instance Methods

assert_bound!() click to toggle source
# File lib/types/enum.rb, line 265
        def assert_bound!
  if @const_name.nil?
    raise "Attempting to access Enum value on #{self.class} before it has been initialized." \
      " Enums are not initialized until the 'enums do' block they are defined in has finished running."
  end
end
comparison_assertion_failed(method, other) click to toggle source
# File lib/types/enum.rb, line 233
        def comparison_assertion_failed(method, other)
  T::Configuration.soft_assert_handler(
    'Enum to string comparison not allowed. Compare to the Enum instance directly instead. See go/enum-migration',
    storytime: {
      class: self.class.name,
      self: self.inspect,
      other: other,
      other_class: other.class.name,
      method: method,
    }
  )
end
const_to_serialized_val(const_name) click to toggle source
# File lib/types/enum.rb, line 280
        def const_to_serialized_val(const_name)
  # Historical note: We convert to lowercase names because the majority of existing calls to
  # `make_accessible` were arrays of lowercase strings. Doing this conversion allowed for the
  # least amount of repetition in migrated declarations.
  const_name.to_s.downcase.freeze
end