Solutions for const and enums per class in Ruby

You may have encountered a situation where you must define constants and enums per class. In this article, we will explore three different solutions.

Active Record Style

One solution is to use ActiveRecord to define enums per class. With this approach, you can define an enum as a class method that takes a hash of symbol-value pairs as arguments. The following code snippet defines an enum called status with three possible values.

require 'active_record'
require 'active_record/enum'

class UserActiveRecordStyle < ActiveRecord::Base
  enum status: { pending: :pending, registered: :registered, deleted: :deleted }
  # or use shorthand
  #enum :status, %i[ pending registered deleted ].index_by(&:itself)

  def self.read_status
    p UserActiveRecordStyle::statuses[:pending] # => :pending
    p UserActiveRecordStyle.statuses # => {:pending=>:pending, :registered=>:registered, :deleted=>:deleted}
    p statuses[:pending] # => :pending
    # p UserActiveRecordStyle::PENDING # => error, not possible
  end
end
UserActiveRecordStyle.read_status

Pros:

  • Easy to use, with a concise syntax for defining enums per class.
  • It provides a built-in way to retrieve the possible enum values as a hash.
  • Automatically adds methods like User.last.pending?, User.last.pending!

Cons:

  • Requires the use of ActiveRecord, which may not be necessary for all projects.
  • Enums defined in this way cannot be accessed as constants (e.g., UserActiveRecordStyle::PENDING is not possible).

Mixed Style

Another solution is to use a combination of constants and enums to define values per class. In this approach, you define an array of constants, each representing a possible value for the enum, and then use a hash to map each constant to its value. See the following example:

class UserActiveRecordStyleMixed < ActiveRecord::Base
  STATUSES = [
    PENDING = :pending.freeze,
    REGISTERED = :registered.freeze,
    DELETED = :deleted.freeze,
  ].freeze

  enum statuses: STATUSES.zip(STATUSES).to_h

  def self.read_status
    p UserActiveRecordStyleMixed::PENDING # => :pending
    p UserActiveRecordStyleMixed.statuses # => {:pending=>:pending, :registered=>:registered, :deleted=>:deleted}
    p UserActiveRecordStyleMixed.statuses[:pending] # => :pending
    p PENDING # => :pending
    p statuses[:pending] # => :pending
    p UserActiveRecordStyleMixed::STATUSES # => [:pending, :registered, :deleted]
  end
end
UserActiveRecordStyleMixed.read_status

Pros:

  • Same as the ActiveRecord style from above
  • Possible use of constants like PENDING, UserActiveRecordStyleMixed::PENDING, etc.

Cons:

  • More to write, but also more possibilities to use
  • Requires the use of two different constructs - constants and enums

PORO Style

A third solution is to use plain old Ruby objects (POROs) to define constants per class. In this approach, you represent an array of constants and then use a hash to map each constant to its value. For example, the following code defines a variety of constants for the status enum and then uses a hash to map each regular to its value:

class UserPoroStyle
  STATUSES = [
    PENDING = :pending.freeze,
    REGISTERED = :registered.freeze,
    DELETED = :deleted.freeze,
  ].freeze

  STATUS = STATUSES.zip(STATUSES).to_h.freeze

  def self.read_status
    p UserPoroStyle::REGISTERED # => :registered
    p UserPoroStyle::STATUSES # => [:pending, :registered, :deleted]
    p REGISTERED # => :registered
    p STATUS # => {:pending=>:pending, :registered=>:registered, :deleted=>:deleted}
    p STATUSES # => [:pending, :registered, :deleted]
    p UserPoroStyle::STATUS # => {:pending=>:pending, :registered=>:registered, :deleted=>:deleted}
    p UserPoroStyle::STATUS[:registered] # => :registered
    p UserPoroStyle::STATUS[REGISTERED] # => :registered
  end
end
UserPoroStyle.read_status

Pros:

  • No need to import ActiveRecord
  • Ability to use constants like PENDING, UserActiveRecordStyleMixed::PENDING, etc.
  • Full control over methods in class

Cons:

  • You need to write your own methods like pending? pending!, etc.

Summary

Each approach has its advantages and disadvantages. The Active Record style is the easiest to use and provides the most built-in functionality. The Mixed style allows you to use constants, which can also be helpful in another part of the code. The PORO style is the most flexible and can be used in non-Active Record code.