Enumerations

Gem Version Maintainability Test Coverage Build Status

Rails plugin for Enumerations in ActiveRecord models.

Installation

Inside your Gemfile add the following:

gem 'enumerations'

Usage

Defining enumerations

Create a model for your enumerations:

class Status < Enumerations::Base
  values draft:           { name: 'Draft' },
         review_pending:  { name: 'Review pending' },
         published:       { name: 'Published' }
end

Or you can use value method for defining your enumerations:

class Status < Enumerations::Base
  value :draft,           name: 'Draft'
  value :review_pending,  name: 'Review pending'
  value :published,       name: 'Published'
end

Include enumerations for integer fields in other models:

class Post < ActiveRecord::Base
  enumeration :status

  validates :status, presence: true
end

You can pass attributes to specify which enumeration and which column to use:

class Post < ActiveRecord::Base
  enumeration :status,
              foreign_key: :post_status,   # specifies which column to use
              class_name: Post::Status     # specifies the class of the enumerator

  validates :post_status, presence: true
end

Attribute foreign_key you can pass as a String or a Symbol. Attribute class_name can be set as a String, a Symbol or a String.

Setting enumeration value to objects

Set enumerations:

@post = Post.first
@post.status = Status.draft
@post.save

Or you can set enumerations by symbol:

@post.status = Status.find(:draft)

If you try to set value that is not an Enumeration value (except nil), you will get an Enumerations::InvalidValueError exception. You can turn this exception off in configuration.

Also, you can set enumeration value like this:

@post.status_draft!

When you include enumerations into your model, you'll get methods for setting each enumeration value. Each method name is consists from enumeration name and enumeration value name with ! at the end. Examples:

class Post < ActiveRecord::Base
  enumeration :status
end

@post.status_draft!
class User < ActiveRecord::Base
  enumeration :role
end

@user.role_admin!
class User < ActiveRecord::Base
  enumeration :type, class_name: Role
end

@user.type_editor!

Finder methods

Find enumerations by id:

@post.status = Status.find(2)                 # => Review pending
@post.save

Other finding methods:

# Find by key as a Symbol
Status.find(:review_pending)                  # => Review pending

# Find by key as a String
Status.find('draft')                          # => Draft

# Find by multiple attributes
Status.find_by(name: 'None', visible: true)   # => None

Compare enumerations:

@post.status == :published                    # => true
@post.status == 'published'                   # => true
@post.status == Status.published              # => true
@post.status.published?                       # => true

Get all enumerations:

Status.all

Filtering methods

Enumerations can be filtered with where method, similar to ActiveRecord::QueryMethods#where.

Role.where(admin: true)                       # => [Role.admin, Role.editor]
Role.where(admin: true, active: true)         # => [Role.admin]

Scopes on model

With enumerations, you'll get scope for each enumeration value in the following format:

with_#{enumeration_name}_#{enumeration_value_name}
without_#{enumeration_name}_#{enumeration_value_name}

or you can use the following scope and pass an array of enumerations:

with_#{enumeration_name}(enumeration, ...)
without_#{enumeration_name}(enumeration, ...)

Examples:

class Post < ActiveRecord::Base
  enumeration :status
end

Post.with_status_draft                        # => <#ActiveRecord::Relation []>
Post.without_status_review_pending            # => <#ActiveRecord::Relation []>
Post.with_status(:draft)                      # => <#ActiveRecord::Relation []>
Post.without_status(:draft)                   # => <#ActiveRecord::Relation []>
Post.with_status(Status.draft)                # => <#ActiveRecord::Relation []>
Post.with_status(:draft, :review_pending)     # => <#ActiveRecord::Relation []>
Post.with_status(Status.draft, 'published')   # => <#ActiveRecord::Relation []>
Post.with_status([:draft, :review_pending])   # => <#ActiveRecord::Relation []>
class Post < ActiveRecord::Base
  enumeration :my_status, class_name: Status
end

Post.with_my_status_draft                      # => <#ActiveRecord::Relation []>
Post.with_my_status_review_pending             # => <#ActiveRecord::Relation []>
Post.with_my_status(:draft)                    # => <#ActiveRecord::Relation []>
Post.without_my_status(:draft)                 # => <#ActiveRecord::Relation []>

Each scope returns all records with specified enumeration value.

Forms usage

Use in forms:

%p
  = f.label :status
  %br
  = f.collection_select :status, Status.all, :symbol, :name

Validating input

Enumerations will by default raise an exception if you try to set an invalid value. This prevents usage of validations, which you might want to add if you're developing an API and have to return meaningful errors to API clients.

You can enable validations by first disabling error raising on invalid input (see configuration). Then, you should add an inclusion validation to enumerated attributes:

class Post < ActiveRecord::Base
  enumeration :status

  validates :status, inclusion: { in: Status.all }
end

You'll now get an appropriate error message when you insert an invalid value:

> post = Post.new(status: 'invalid')
> post.valid?
=> false
> post.errors.full_messages.to_sentence
=> "Status is not included in the list"
> post.status
=> "invalid"

Advanced Usage

Except name you can specify any other attributes to your enumerations:

class Status < Enumerations::Base
  value :draft,           id: 1, name: 'Draft', published: false
  value :review_pending,  id: 2, name: 'Review pending', description: 'Some description...'
  value :published,       id: 3, name: 'Published', published: true
  value :other                                 # passing no attributes is also allowed
end

Every enumeration has id, name, description and published methods. If you call method that is not in attribute list for enumeration, it will return nil.

Status.review_pending.description              # => 'Some description...'
Status.draft.description                       # => nil

For each attribute, you have predicate method. Predicate methods are actually calling present? method on attribute value:

Status.draft.name?                             # => true
Status.draft.published?                        # => false
Status.published.published?                    # => true
Status.other.name?                             # => false

Translations

Enumerations uses power of I18n API (if translate_attributes configuration is set to true) to enable you to create a locale file for enumerations like this:

---
en:
  enumerations:
    status:
      draft:
        name: Draft
        description: Article draft...
        ...
    role:
      admin:
        name: Administrator

You can separate enumerations locales into a separate *.yml files. Then you should add locale file paths to I18n load path:

# config/initializers/locale.rb

# Where the I18n library should search for translation files (e.g.):
I18n.load_path += Dir[Rails.root.join('config', 'locales', 'enumerations', '*.yml')]

Configuration

Basically no configuration is needed.

Enumerations has four configuration options. You can customize primary key, foreign key suffix, whether to translate attributes and whether to raise Enumerations::InvalidValueError exception when setting invalid values. Just add initializer file to config/initializers/enumerations.rb.

Example of configuration:

# config/initializers/enumerations.rb

Enumerations.configure do |config|
  config.primary_key               = :id
  config.foreign_key_suffix        = :id
  config.translate_attributes      = true
  config.raise_invalid_value_error = true
end

By default, primary_key and foreign_key_suffix are set to nil.

By default model enumeration value is saved to column with same name as enumeration. If you set enumeration as:

enumeration :status

then model should have status column (as String type). If you want save an ID to this column, you can set foreign_key_suffix to id. Then model should have status_id column.

If you set primary_key then you need provide this attribute for all enumerations values. Also, value from primary_key attribute will be stored to model as enumeration value.

For example:

# with default configuration

post = Post.new
post.status = Status.draft      # => post.status = 'draft'

# with configured primary_key and foreign_key_suffix:

Enumerations.configure do |config|
  config.primary_key        = :id
  config.foreign_key_suffix = :id
end

class Status < Enumerations::Base
  value :draft,           id: 1, name: 'Draft'
  value :review_pending,  id: 2, name: 'Review pending',
  value :published,       id: 3, name: 'Published', published: true
end

post = Post.new
post.status = Status.draft      # => post.status_id = 1

If you want to configure primary key per enumeration class, you can use primary_key= class method:

class Status < Enumerations::Base
  self.primary_key = :id

  value :draft,           id: 1, name: 'Draft'
  value :review_pending,  id: 2, name: 'Review pending'
end

Database Enumerations

By default, enumeration values are saved to database as String. If you want, you can define Enum type in database:

CREATE TYPE status AS ENUM ('draft', 'review_pending', 'published');

Then you'll have enumeration as type in database and you can use it in database migrations:

add_column :posts, :status, :status, index: true

With configuration option primary_key, you can store any type you want (e.g. Integer).

Also, for performance reasons, you should add indices to enumeration column.

Here you can find more informations about ENUM types.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Credits

Enumerations is maintained and sponsored by Infinum

Copyright © 2010 - 2018 Infinum Ltd.

License

The gem is available as open source under the terms of the MIT License.