EnumerateIt
Ruby Enumerations with magic powers!
EnumerateIt helps you to declare and use enumerations in a very simple and flexible way.
Why would I want a gem if Rails already has native enumerations support?
Firstly, EnumerateIt works amazingly well along with Rails but it is not required! It means you can add it to any Ruby project! 😀
Secondly, Rails' enumerations have a problem:
ActiveRecord::Enum uses
integers instead of strings, which means that if you change your enumeration list oder in your
model, your database will no longer be consistent.
Database storage pricing is not a problem nowadays, so it's recommended to use strings columns.
- Installation
- Using with Rails
- Creating enumerations
- Using enumerations
- I18n
- Using enumerations to handle a legacy database
- FAQ
- Changelog
Installation
gem install enumerate_it
Using with Rails
Add the gem to your Gemfile:
gem 'enumerate_it'
Run the install generator:
rails generate enumerate_it:install
There is also a Rails Generator which generates enumerations and their locale files:
rails generate enumerate_it:enum --help
Creating enumerations
Enumerations are created as classes and you should put them inside app/enumerations folder.
You can pass an array of symbols, so that the respective value for each symbol will be the stringified version of the symbol itself:
class RelationshipStatus < EnumerateIt::Base
associate_values(
:single,
:married,
:divorced
)
end
This will create some nice stuff:
- Each enumeration's value will turn into a constant:
RelationshipStatus::SINGLE
# => 'single'
RelationshipStatus::MARRIED
#=> 'married'
- You can retrieve a list with all the enumeration codes:
RelationshipStatus.list
#=> ['divorced', 'married', 'single']
- You can retrieve a JSON with all the enumeration codes:
RelationshipStatus.to_json
#=> "[{\"value\":\"divorced\",\"label\":\"Divorced\"},{\"value\":\"married\", ...
- You can get an array of options, ready to use with the 'select', 'select_tag', etc family of Rails helpers.
RelationshipStatus.to_a
#=> [['Divorced', 'divorced'], ['Married', 'married'], ['Single', 'single']]
- You can retrieve a list with values for a group of enumeration constants.
RelationshipStatus.values_for %w(MARRIED SINGLE)
#=> ['married', 'single']
- You can retrieve the value for a specific enumeration constant:
RelationshipStatus.value_for('MARRIED')
#=> 'married'
- You can retrieve the symbol used to declare a specific enumeration value:
RelationshipStatus.key_for(RelationshipStatus::MARRIED)
#=> :married
- You can iterate over the list of the enumeration's values:
RelationshipStatus.each_value { |value| ... }
- You can iterate over the list of the enumeration's translations:
RelationshipStatus.each_translation { |translation| ... }
- You can also retrieve all the translations of the enumeration:
RelationshipStatus.translations
- You can ask for the enumeration's length:
RelationshipStatus.length
#=> 3
Sorting enumerations
When calling methods like to_a, to_json and list, the returned values will be sorted using
the translation for each one of the enumeration values. If you want to overwrite the default sort
mode, you can use the sort_by class method.
class RelationshipStatus < EnumerateIt::Base
associate_values married: 1, single: 2
sort_by :translation
end
The sort_by methods accept one of the following values:
| Value | Behavior |
|---|---|
:none |
The default behavior, will return values in order that was passed to associate_values call |
:translation |
will sort the returned values based on translations |
:name |
Will sort the returned values based on the name of each enumeration option |
Using enumerations
The cool part is that you can use these enumerations with any class, be it an ActiveRecord
instance or not.
class Person
extend EnumerateIt
attr_accessor :relationship_status
has_enumeration_for :relationship_status
end
Note: EnumerateIt will try to load an enumeration class based on the camelized attribute name. If you have a different name, you can specify it by using the
withoption:
has_enumeration_for :relationship_status, with: RelationshipStatus
This will create:
- A "humanized" version of the hash's key to humanize the attribute's value:
p = Person.new
p.relationship_status = RelationshipStatus::DIVORCED
p.relationship_status_humanize
#=> 'Divorced'
- A translation for your options, if you include a locale to represent it (see more in the #i18n:
p = Person.new
p.relationship_status = RelationshipStatus::DIVORCED
p.relationship_status_humanize
#=> 'Divorciado'
- The associated enumerations, which can be retrieved with the
enumerationsclass method:
Person.enumerations
#=> { relationship_status: RelationshipStatus }
- A helper method for each enumeration option, if you pass the
create_helpersoption astrue:
class Person < ActiveRecord::Base
has_enumeration_for :relationship_status, with: RelationshipStatus, create_helpers: true
end
p = Person.new
p.relationship_status = RelationshipStatus::MARRIED
p.married?
#=> true
p.divorced?
#=> false
It's also possible to "namespace" the created helper methods, passing a hash to the create_helpers
option. This can be useful when two or more of the enumerations used share the same constants:
class Person < ActiveRecord::Base
has_enumeration_for :relationship_status,
with: RelationshipStatus, create_helpers: { prefix: true }
end
p = Person.new
p.relationship_status = RelationshipStatus::MARRIED
p.relationship_status_married?
#=> true
p.relationship_status_divorced?
#=> false
You can define polymorphic behavior for the enumeration values, so you can define a class for each of them:
class RelationshipStatus < EnumerateIt::Base
associate_values :married, :single
class Married
def saturday_night
'At home with the kids'
end
end
class Single
def saturday_night
'Party Hard!'
end
end
end
class Person < ActiveRecord::Base
has_enumeration_for :relationship_status,
with: RelationshipStatus, create_helpers: { polymorphic: true }
end
p = Person.new
p.relationship_status = RelationshipStatus::MARRIED
p.relationship_status_object.saturday_night
#=> 'At home with the kids'
p.relationship_status = RelationshipStatus::SINGLE
p.relationship_status_object.saturday_night
#=> 'Party Hard!'
You can also change the suffix _object, using the suffix option:
class Person < ActiveRecord::Base
has_enumeration_for :relationship_status,
with: RelationshipStatus, create_helpers: { polymorphic: { suffix: '_mode' } }
end
p.relationship_status_mode.saturday_night
The create_helpers also creates some mutator helper methods, that can be used to change the
attribute's value.
p = Person.new
p.married!
p.married?
#=> true
- A scope method for each enumeration option if you pass the
create_scopesoption astrue:
class Person < ActiveRecord::Base
has_enumeration_for :relationship_status, with: RelationshipStatus, create_scopes: true
end
Person.married.to_sql
#=> SELECT "users".* FROM "users" WHERE "users"."relationship_status" = "married"
The :create_scopes also accepts :prefix option.
class Person < ActiveRecord::Base
has_enumeration_for :relationship_status,
with: RelationshipStatus, create_scopes: { prefix: true }
end
Person.relationship_status_married.to_sql
- An inclusion validation (if your class can manage validations and responds to
validates_inclusion_of):
class Person < ActiveRecord::Base
has_enumeration_for :relationship_status, with: RelationshipStatus
end
p = Person.new(relationship_status: 'invalid')
p.valid?
#=> false
p.errors[:relationship_status]
#=> 'is not included in the list'
- An presence validation (if your class can manage validations and responds to
validates_presence_ofand you pass therequiredoptions astrue):
class Person < ActiveRecord::Base
has_enumeration_for :relationship_status, required: true
end
p = Person.new relationship_status: nil
p.valid?
#=> false
p.errors[:relationship_status]
#=> "can't be blank"
If you pass the skip_validation option as true, it will not create any validations:
class Person < ActiveRecord::Base
has_enumeration_for :relationship_status, with: RelationshipStatus, skip_validation: true
end
p = Person.new(relationship_status: 'invalid')
p.valid?
#=> true
Remember that you can add validations to any kind of class and not only to those derived from
ActiveRecord::Base.
Why to define enumerations outside the class that uses them?
- It's clearer.
- You can add behaviour to the enumeration class.
- You can reuse the enumeration inside other classes.
I18n
I18n lookup is provided on both _humanized and Enumeration#to_a methods, given the hash key is
a Symbol. The I18n strings are located on enumerations.<enumeration_name>.<key>:
# Your locale file
pt-BR:
enumerations:
relationship_status:
married: Casado
class RelationshipStatus < EnumerateIt::Base
associate_values(
:married,
:single
)
end
p = Person.new
p.relationship_status = RelationshipStatus::MARRIED
p.relationship_status_humanize
#=> 'Casado'
p.relationship_status = RelationshipStatus::SINGLE
p.relationship_status_humanize # Non-existent key
#=> 'Single'
You can also translate specific values:
status = RelationshipStatus::MARRIED
RelationshipStatus.t(status)
#=> 'Casado'
Using enumerations to handle a legacy database
EnumerateIt can help you build a Rails application around a legacy database which was filled with those small and unchangeable tables used to create foreign key constraints everywhere, like the following example:
Table "public.relationship_status"
Column | Type | Modifiers
-------------+---------------+-----------
code | character(1) | not null
description | character(11) |
Indexes:
"relationship_status_pkey" PRIMARY KEY, btree (code)
SELECT * FROM relationship_status;
code | description
---- +--------------
1 | Single
2 | Married
3 | Divorced
You might also have something like a users table with a relationship_status column and a foreign
key pointing to the relationship_status table.
While this is a good thing from the database normalization perspective, managing these values in
tests is very hard. Doing database joins just to get the description of some value is absurd.
And, more than this, referencing them in the code using magic numbers was terrible and meaningless:
What does it mean when we say that someone or something is 2?
In this case, you can pass a hash:
class RelationshipStatus < EnumerateIt::Base
associate_values(
single: 1,
married: 2,
divorced: 3
)
end
RelationshipStatus::MARRIED
#=> 2
You can also sort it by its value: sort_by :value.
FAQ
What versions of Ruby and Rails are supported?
Please check out travis config file.
Can I set a value to always be at the end of a sorted list?
Yes, please see issue #60.
Changelog
Changes are maintained under Releases.
Note on Patches/Pull Requests
- Fork the project.
- Make your feature addition or bug fix.
- Add tests for it. This is important so we don't break it in a future version unintentionally.
- [Optional] Run the tests agaist a specific Gemfile:
$ appraisal rails_5.0 rake spec. - Run the tests agaist all supported versions:
$ rake. - Commit, but please do not mess with
Rakefile, version, or history. - Send a Pull Request. Bonus points for topic branches.
Copyright
Copyright (c) 2010-2016 Cássio Marques and Lucas Caton. See LICENSE file for details.