SwifterEnum

SwifterEnum is a Ruby gem for creating enumerated types (enums) in Ruby on Rails applications.

It is inspired by Swift's enums, and allows you to keep logic related to your enum directly in your enum class.

so - after defining

class Video < ApplicationRecord
  swifter_enum :camera, CameraEnum
end

you can then define and access methods on your enum like

video.camera.icon

This avoids helper methods which distribute your enum logic around your application.

Before

helper method somewhere in the app

#app/helpers/controller_helper.rb

def icon_for(camera:)
  ...
end

called with

icon_for(camera:my_model.camera)

After

logic encapsluated within the enum class

#app/models/swifter_enum/camera_enum.rb
class CameraEnum < SwifterEnum::Base
  set_values ({ videographer: 0, handcam: 1 })

  def icon
    ...
  end
end

called with

my_model.camera.icon

I was prompted to create this gem by reading about enum approaches in the RailsNotes Newsletter. Like any good programmer, none of those solutions quite met my requirements. Hopefully it will be useful. I welcome feedback, fixes and pull requests.

Installation

Add this line to your application's Gemfile:

gem 'swifter_enum'

Usage

Overview

SwifterEnums act like a normal Rails enum - except that instead of returning string values, they return an instance of your selected class.

They also have various affordances so that in many cases, you can treat them as if they return symbol values.

We have a Video ActiveModel with an enum defined by

class Video < ApplicationRecord
  swifter_enum :camera, CameraEnum
end

CameraEnum is a class like the following

class CameraEnum < SwifterEnum::Base
  set_values ({ videographer: 0, handcam: 1 })

  def icon
    case @value
    when :videographer
      "icons/video-camera"
    when :handcam
      "icons/hand-stop"
    end
  end
end

This provides a richer approach to enums:

v = Video.first
v.camera => #<CameraEnum:0x0000000134c7c290 @value=:handcam> 
v.camera.value => :handcam 

#you can set values directly
v.camera = :videographer
v.camera => #<CameraEnum:0x000000013385f078 @value=:videographer> 

#the purpose of this gem is that you can now define and access methods on the CameraEnum
v.camera.icon => "icons/video-camera" 

Using Enums in ActiveRecord Models

To use an enum in an ActiveRecord model, use the swifter_enum class method provided by the gem in exactly the same way that you would normally use enum

Example:

class Video < ApplicationRecord
  swifter_enum :camera, CameraEnum

  #optional validation
  validates :camera, swifter_enum: true
end

Models are by convention stored in /models/swifter_enum/your_model_enum.rb

Enum Class

Example:

class CameraEnum < SwifterEnum::Base
  set_values ({ videographer: 0, handcam: 1 })

  def icon
    case @value
    when :videographer
      "icons/video-camera"
    when :handcam
      "icons/hand-stop"
    end
  end
end

The only requirements for your enum class are

  • Inherit from SwifterEnum::Base
  • Define self.values which returns a hash of {symbol: Integer}

You can then add whatever methods are useful to you.

Migrating an existing enum

let's say you have an existing enum

enum :album_status, {
  waiting_for_footage: 0,
  waiting_for_upload: 10,
  uploading: 20,
  processing_preview: 24,
  delivered_preview: 26,
  processing_paid: 30,
  delivered_paid: 50,
  processing_failed: 60
}, prefix: true

run the generator to create an appropriate enum class

rails g swifter_enum:enum AlbumStatus

Insert the values of your enum into models/swifter_enum_album_status_enum.rb

class AlbumStatusEnum < SwifterEnum::Base
  set_values ({
                waiting_for_footage: 0,
                waiting_for_upload: 10,
                uploading: 20,
                processing_preview: 24,
                delivered_preview: 26,
                processing_paid: 30,
                delivered_paid: 50,
                processing_failed: 60
              })
end

Now replace the definition in your model file with

swifter_enum :album_status, AlbumStatusEnum, prefix: true

(note - prefix: optional. I'm adding it here because it was an option I used on my original standard Rails enum)

Optionally, add

validates :album_status, swifter_enum: true

Run your tests and fix any issues.

The main change is where you were assuming that your enum would return a string value.

Typically, in my code, I would convert these to a symbol before comparing. So, I have to remove album_status.to_sym calls.

Now I can use album_status.value to get a symbol value,

album_status.value => :uploading

or if I'm doing a comparison - I can just use album_status.

if album_status == :uploading {} #works as expected
if album_status == 'uploading' {} #also works as expected

Now I'm ready to use my enum class by defining new methods.

For example

class AlbumStatusEnum < SwifterEnum::Base

  #values set as above

  def delivered?
    [:delivered_paid, :delivered_preview].include? self.value
  end

end

which allows me to use model.album_status.delivered?

Generator Usage

SwifterEnum provides a Rails generator to easily create new enum classes. To generate an enum, use the following command:

rails g swifter_enum:enum [EnumName]

Replace [EnumName] with the desired name for your enum. This will create a new enum file in the app/models directory.

For example, to create a CameraEnum, run:

rails g swifter_enum:enum Camera

This command will generate a file app/models/swifter_enum/camera_enum.rb with the following structure:

class CameraEnum < SwifterEnum::Base
  set_values <<Your Values Here>>
end

After generating your enum, you can add your specific enum values and use it in your ActiveRecord models.

Translation and Display

SwifterEnum supports i18n out of the box. Define translations in your locale files, and use the .t method on your enum instances to get the translated string.

Locale file example (config/locales/en.yml):

en:
  swifter_enum:
    enums:
      camera_enum:
        videographer: "Videographer"
        handcam: "Handheld Camera"

#example usage
v.camera.t => "Videographer"

Using string values to store Enum

DHH has described using integer values for enums as a mistake he regrets.

He has shown code like

enum direction: %w[ up down left right ].index_by(&:itself)

which uses string values in the db by generating the hash {"up"=>"up", "down"=>"down", "left"=>"left", "right"=>"right"}

Swifter enum allows the same - but lets you simply set your values using an array of strings or symbols

set_values %w[ up down left right]
#or
set_values [:up,:down,:left,:right]

Raw Value Escape Hatch

SwifterEnum is built on top of the normal Rails enum functionality.

If you're using a framework that needs to access this (Like Administrate), then you can use the [name]_raw method

This also provides the keys method for select forms, so form builders should work (though the label will be [name]_raw rather than [name])

So, if you define Video.camera as a CameraEnum, then Video.camera_raw returns the standard Rails enum which lies beneath.

Example:

v.camera  => #<CameraEnum:0x000000013385f078 @value=:videographer> 

# Accessing the raw value
v.camera_raw  => "videographer" 

# Setting the raw value
v.camera_raw = "handcam" #or =:handcam
v.camera =  => #<CameraEnum:0x000000016f7223e0 @value=:handcam> 

v.camera_raw = 0
v.camera =  => #<CameraEnum:0x000000016f7223e0 @value=:videographer> 

# Getting the mapping
Video.camera_raws # => { videographer: 0, handcam: 1 }

so, for my Administrate dashboard, I would use

album_status_raw: Field::Select.with_options(searchable: false, collection: ->(field) { field.resource.class.send(field.attribute.to_s.pluralize).keys }),

More Info

See the tests folder for more examples of usage.

Contributing

Bug reports and pull requests are welcome