class T::Enum
should be kept immutable to avoid unpredictable action at a distance.
WARNING: Enum instances are singletons that are shared among all their users. Their internals
def is_red?(suit); …; end
sig {params(suit: Suit).returns(Boolean)}
@example Using enums in type signatures:
Suit.deserialize(‘club’) == Suit::CLUB
@example Converting from serialized value to enum instance:
Suit::SPADE
@example Accessing values:
end
end
…
READY = new(‘rdy’)
enums do
class Status < T::Enum
@example Custom serialization value:
end
end
HEART = new
DIAMOND = new
SPADE = new
CLUB = new
enums do
class Suit < T::Enum
@example Declaring an Enum:
constructor. Enum will ‘freeze` the serialized value.
to lowercase (e.g. `Suit::Club.serialize == ’club’‘); however a custom value may be passed to the
Each value has a corresponding serialized value. By default this is the constant’s name converted
Every value is a singleton instance of the class (i.e. ‘Suit::SPADE.is_a?(Suit) == true`).
Enumerations allow for type-safe declarations of a fixed set of values.
def self._load(args)
def self._load(args) deserialize(Marshal.load(args)) end
def self._register_instance(instance)
def self._register_instance(instance) @values ||= [] @values << T.cast(instance, T.attached_class) end
def self.deserialize(mongo_value)
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
def self.each_value(&blk)
def self.each_value(&blk) if blk values.each(&blk) else values.each end end
def self.enums(&blk)
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
def self.from_serialized(serialized_val)
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
def self.fully_initialized?
def self.fully_initialized? @fully_initialized = T.let(@fully_initialized, T.nilable(T::Boolean)) @fully_initialized ||= false end
def self.has_serialized?(serialized_val)
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
def self.inherited(child_class)
def self.inherited(child_class) super raise "Inheriting from children of T::Enum is prohibited" if self != T::Enum end
def self.serialize(instance)
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
def self.started_initializing?
def self.started_initializing? @started_initializing = T.let(@started_initializing, T.nilable(T::Boolean)) @started_initializing ||= false end
def self.try_deserialize(serialized_val)
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
def self.values
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
def <=>(other)
def <=>(other) case other when self.class self.serialize <=> other.serialize else nil end end
def ==(other)
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
def ===(other)
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
def _bind_name(const_name)
def _bind_name(const_name) @const_name = const_name @serialized_val = const_to_serialized_val(const_name) if @serialized_val.nil? freeze end
def _dump(_level)
def _dump(_level) Marshal.dump(serialize) end
def assert_bound!
def assert_bound! nst_name.nil? e "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."
def clone
def clone self end
def comparison_assertion_failed(method, other)
def comparison_assertion_failed(method, other) figuration.soft_assert_handler( m to string comparison not allowed. Compare to the Enum instance directly instead. See go/enum-migration', ytime: { ass: self.class.name, lf: self.inspect, her: other, her_class: other.class.name, thod: method,
def const_to_serialized_val(const_name)
def const_to_serialized_val(const_name) orical note: We convert to lowercase names because the majority of existing calls to e_accessible` were arrays of lowercase strings. Doing this conversion allowed for the t amount of repetition in migrated declarations. name.to_s.downcase.freeze
def dup
def dup self end
def initialize(serialized_val=nil)
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
def inspect
def inspect "#<#{self.class.name}::#{@const_name || '__UNINITIALIZED__'}>" end
def serialize
def serialize assert_bound! @serialized_val end
def to_json(*args)
def to_json(*args) serialize.to_json(*args) end
def to_s
def to_s inspect end
def to_str
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