Enumerated attributes with I18n and ActiveRecord/Mongoid/MongoMapper/Sequel support
Table of Contents
- Installation
- Supported Versions
- Usage
- Database support
- ActiveRecord
- Mongoid
- MongoMapper
- I18n Support
- I18n Helper Methods
- Boolean Helper Methods
- Basic
- Predicate Methods
- Optimzations and Tips
- Extendable Module
- Customizing Enumerize Value
- ActiveRecord scopes
- Array-like Attributes
- Forms
- SimpleForm
- Formtastic
- Testing
- RSpec
- Minitest with Shoulda
- Other Integrations
- Contributing
Installation
Add this line to your application's Gemfile:
gem 'enumerize'
And then execute:
$ bundle
Or install it yourself as:
$ gem install enumerize
Supported Versions
- Ruby 3.0+
- Rails 6.1+
Usage
Basic:
class User
extend Enumerize
enumerize :role, in: [:user, :admin]
end
Note that enumerized values are just identifiers so if you want to use multi-word, etc. values then you should use I18n
feature.
Database support
ActiveRecord
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :status
t.string :role
t.timestamps
end
end
end
class User < ActiveRecord::Base
extend Enumerize
enumerize :status, in: [:student, :employed, :retired], default: lambda { |user| StatusIdentifier.status_for_age(user.age).to_sym }
enumerize :role, in: [:user, :admin], default: :user
end
⚠️ By default, enumerize
adds inclusion
validation to the model. You can skip validations by passing skip_validations
option. ⚠️
class User < ActiveRecord::Base
extend Enumerize
enumerize :status, in: [:student, :employed, :retired], skip_validations: lambda { |user| user.new_record? }
enumerize :role, in: [:user, :admin], skip_validations: true
end
Mongoid
class User
include Mongoid::Document
extend Enumerize
field :role
enumerize :role, in: [:user, :admin], default: :user
end
MongoMapper
class User
include MongoMapper::Document
extend Enumerize
key :role
enumerize :role, in: [:user, :admin], default: :user
end
I18n Support
en:
enumerize:
user:
status:
student: "Student"
employed: "Employed"
retired: "Retiree"
or if you use status
attribute across several models you can use defaults
scope:
en:
enumerize:
defaults:
status:
student: "Student"
employed: "Employed"
retired: "Retiree"
You can also pass i18n_scope
option to specify scope (or array of scopes) storing the translations.
class Person
extend Enumerize
extend ActiveModel::Naming
enumerize :status, in: %w[student employed retired], i18n_scope: "status"
enumerize :roles, in: %w[user admin], i18n_scope: ["user.roles", "roles"]
enumerize :color, in: %w[green blue], i18n_scope: proc { |value| "color" }
end
# localization file
en:
status:
student: "Student"
employed: "Employed"
retired: "Retiree"
user:
roles:
user: "User"
roles:
admin: "Admin"
Note that if you want to use I18n feature with plain Ruby object don't forget to extend it with ActiveModel::Naming
:
class User
extend Enumerize
extend ActiveModel::Naming
end
I18n Helper Methods
*_text / .text
Attribute's I18n text value:
@user.status_text # or @user.status.text
values
List of possible values for an enumerized attribute:
User.status.values # or User.enumerized_attributes[:status].values
# => ['student', 'employed', 'retired']
I18n text values
List of possible I18n text values for an enumerized attribute:
User.status.values.collect(&:text)
# => ['Student', 'Employed', 'Retiree']
Form example
Use it with forms (it supports :only
and :except
options):
<%= form_for @user do |f| %>
<%= f.select :status, User.status.options %>
<% end %>
Boolean Helper Methods
Basic
user.status = :student
user.status.student? #=> true
user.status.retired? #=> false
Predicate Methods
class User
extend Enumerize
enumerize :status, in: %w(student employed retired), predicates: true
end
user = User.new
user.student? # => false
user.employed? # => false
user.status = :student
user.student? # => true
user.employed? # => false
⚠️ If enumerize
is used with Mongoid, it's not recommended to use "writer"
as a field value since writer?
is defined by Mongoid. See more. ⚠️
Predicate Prefixes
class User
extend Enumerize
enumerize :status, in: %w(student employed retired), predicates: { prefix: true }
end
user = User.new
user.status = 'student'
user.status_student? # => true
Use :only
and :except
options to specify what values create predicate methods for.
Optimzations and Tips
Extendable Module
To make some attributes shared across different classes it's possible to define them in a separate module and then include it into classes:
module RoleEnumerations
extend Enumerize
enumerize :roles, in: %w[user admin]
end
class Buyer
include RoleEnumerations
end
class Seller
include RoleEnumerations
end
Customizing Enumerize Value
It's also possible to store enumerized attribute value using custom values (e.g. integers). You can pass a hash as :in
option to achieve this:
class User < ActiveRecord::Base
extend Enumerize
enumerize :role, in: { user: 1, admin: 2 }
end
user = User.new
user.role = :user
user.role #=> 'user'
user.role_value #=> 1
User.role.find_value(:user).value #=> 1
User.role.find_value(:admin).value #=> 2
ActiveRecord scopes:
Basic
class User < ActiveRecord::Base
extend Enumerize
enumerize :role, in: [:user, :admin], scope: true
enumerize :status, in: { student: 1, employed: 2, retired: 3 }, scope: :having_status
end
User.with_role(:admin)
# SELECT "users".* FROM "users" WHERE "users"."role" IN ('admin')
User.without_role(:admin)
# SELECT "users".* FROM "users" WHERE "users"."role" NOT IN ('admin')
User.having_status(:employed).with_role(:user, :admin)
# SELECT "users".* FROM "users" WHERE "users"."status" IN (2) AND "users"."role" IN ('user', 'admin')
Shallow Scopes
Adds named scopes to the class directly.
class User < ActiveRecord::Base
extend Enumerize
enumerize :status, in: [:student, :employed, :retired], scope: :shallow
enumerize :role, in: { user: 1, admin: 2 }, scope: :shallow
end
User.student
# SELECT "users".* FROM "users" WHERE "users"."status" = 'student'
User.admin
# SELECT "users".* FROM "users" WHERE "users"."role" = 2
⚠️ It is not possible to define a scope when using the :multiple
option. ⚠️
Array-like Attributes
Array-like attributes with plain ruby objects:
class User
extend Enumerize
enumerize :interests, in: [:music, :sports], multiple: true
end
user = User.new
user.interests << :music
user.interests << :sports
and with ActiveRecord:
class User < ActiveRecord::Base
extend Enumerize
serialize :interests, Array
enumerize :interests, in: [:music, :sports], multiple: true
end
get an array of all text values:
@user.interests.texts # shortcut for @user.interests.map(&:text)
Also, the reader method can be overridden, referencing the enumerized attribute value using super
:
def status
if current_user.admin?
"Super #{super}"
else
super
end
end
Forms
SimpleForm
If you are using SimpleForm gem you don't need to specify input type (:select
by default) and collection:
<%= simple_form_for @user do |f| %>
<%= f.input :status %>
<% end %>
and if you want it as radio buttons:
<%= simple_form_for @user do |f| %>
<%= f.input :status, as: :radio_buttons %>
<% end %>
Please note that Enumerize overwrites the I18n keys of SimpleForm collections. The enumerized keys are used instead of the SimpleForm ones for inputs concerning enumerized attributes. If you don't want this just pass :collection
option to the input
call.
Formtastic
If you are using Formtastic gem you also don't need to specify input type (:select
by default) and collection:
<%= semantic_form_for @user do |f| %>
<%= f.input :status %>
<% end %>
and if you want it as radio buttons:
<%= semantic_form_for @user do |f| %>
<%= f.input :status, as: :radio %>
<% end %>
Testing
RSpec
Also you can use builtin RSpec matcher:
class User
extend Enumerize
enumerize :status, in: [:student, :employed, :retired]
end
describe User do
it { should enumerize(:status) }
# or with RSpec 3 expect syntax
it { is_expected.to enumerize(:status) }
end
Qualifiers
in
Use in
to test usage of the :in
option.
class User
extend Enumerize
enumerize :status, in: [:student, :employed, :retired]
end
describe User do
it { should enumerize(:status).in(:student, :employed, :retired) }
end
You can test enumerized attribute value using custom values with the in
qualifier.
class User
extend Enumerize
enumerize :role, in: { user: 0, admin: 1 }
end
describe User do
it { should enumerize(:role).in(user: 0, admin: 1) }
end
with_default
Use with_default
to test usage of the :default
option.
class User
extend Enumerize
enumerize :role, in: [:user, :admin], default: :user
end
describe User do
it { should enumerize(:user).in(:user, :admin).with_default(:user) }
end
with_i18n_scope
Use with_i18n_scope
to test usage of the :i18n_scope
option.
class User
extend Enumerize
enumerize :status, in: [:student, :employed, :retired], i18n_scope: 'status'
end
describe User do
it { should enumerize(:status).in(:student, :employed, :retired).with_i18n_scope('status') }
end
with_predicates
Use with_predicates
to test usage of the :predicates
option.
class User
extend Enumerize
enumerize :status, in: [:student, :employed, :retired], predicates: true
end
describe User do
it { should enumerize(:status).in(:student, :employed, :retired).with_predicates(true) }
end
You can text prefixed predicates with the with_predicates
qualifiers.
class User
extend Enumerize
enumerize :status, in: [:student, :employed, :retired], predicates: { prefix: true }
end
describe User do
it { should enumerize(:status).in(:student, :employed, :retired).with_predicates(prefix: true) }
end
with_scope
Use with_scope
to test usage of the :scope
option.
class User
extend Enumerize
enumerize :status, in: [:student, :employed, :retired], scope: true
end
describe User do
it { should enumerize(:status).in(:student, :employed, :retired).with_scope(true) }
end
You can test a custom scope with the with_scope
qualifiers.
class User
extend Enumerize
enumerize :status, in: [:student, :employed], scope: :employable
end
describe User do
it { should enumerize(:status).in(:student, :employed, :retired).with_scope(scope: :employable) }
end
with_multiple
Use with_multiple
to test usage of the :multiple
option.
class User
extend Enumerize
enumerize :status, in: [:student, :employed, :retired], multiple: true
end
describe User do
it { should enumerize(:status).in(:student, :employed, :retired).with_multiple(true) }
end
Minitest with Shoulda
You can use the RSpec matcher with shoulda in your tests by adding two lines in your test_helper.rb
inside class ActiveSupport::TestCase
definition:
class ActiveSupport::TestCase
ActiveRecord::Migration.check_pending!
require 'enumerize/integrations/rspec'
extend Enumerize::Integrations::RSpec
...
end
Other Integrations
Enumerize integrates with the following automatically:
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Added some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request