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)) # rubocop:disable Security/MarshalLoad 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? unless defined?(@fully_initialized) @fully_initialized = T.let(false, T.nilable(T::Boolean)) end T.must(@fully_initialized) 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 # "oj" gem JSON support if Object.const_defined?(:Oj) Object.const_get(:Oj).register_odd(child_class, child_class, :try_deserialize, :serialize) end 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? unless defined?(@started_initializing) @started_initializing = T.let(false, T.nilable(T::Boolean)) end T.must(@started_initializing) 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 _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 as_json(*args)
def as_json(*args) serialized_val = serialize return serialized_val unless serialized_val.respond_to?(:as_json) serialized_val.as_json(*args) 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 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, caller_location: Kernel.caller_locations(1..1)&.[](0)&.then {"#{_1.path}:#{_1.lineno}"}, }, ) serialize.to_s else Kernel.raise NoMethodError.new(msg) end end