Enu
This gem introduces missing enumerated type for Ruby and Rails.
Purpose and features:
- Unify enum types definition for Rails model attributes, compatible with ActiveRecord's enum declarations.
- Use structured constants instead of magic strings or numbers to address enum values.
- Keep track on enum references to simplify refactoring and codebase navigation.
- Provide a standardized way to export enum definitions to client-side JavaScript modules, managed by either Webpack or Rails Asset Pipeline.
Installation
Add this line to your application's Gemfile:
gem 'enu'And then execute:
$ bundleOr install it yourself as:
$ gem install enuUsage
Enum types definition
Here is a basic example:
# app/enums/post_status.rb
class PostStatus < Enu
option :draft
option :published
option :moderated
option :deleted
endThis class defines an enum type for a blog post status with a set of possible states: draft, published, moderated and deleted. Each state automatically receives integer representation: 0 for draft, 1 for published, and so on. The top option will be treated as default.
It is also possible to specify explicit integer values:
class PostStatus < Enu
option :draft, 10
option :published, 20
endOr mix implicit and explicit approach:
class PostStatus < Enu
option :draft, 10
option :published
endEnu will ensure there are no collisions in the option names and values.
Using enums
Enu classes are compatible with ActiveRecord's enum declaration:
class Post < ApplicationRecord
enum status: PostStatus
end
# Use enum helpers as usual:
post = Post.create! # => #<Post:...>
Post.draft? # => true
post.published? # => false
Post.published.to_sql # => "SELECT "posts".* FROM "posts" WHERE "posts"."status" = 1"Each Enu descendant inherits options class method, returning the options hash. In addition the enumeration class delegates some Hash methods, so ActiveRecord can treat it as an actual hash. In the last example enum status: PostStatus call is equivalent to enum status: PostStatus.options:
PostStatus.options # => {:draft=>0, :published=>1, :moderated=>2, :deleted=>3}
PostStatus.each.to_h # => {:draft=>0, :published=>1, :moderated=>2, :deleted=>3}Scoped constants
Sometimes ApplicationRecord helpers are not enough, and you need to address enum values directly. If this is the case, use scoped constants instead of magic strings or symbol values.
Each option definition generates matching value method:
PostStatus.draft # => :draft
PostStatus.published # => :published
PostStatus.moderated # => :moderated
PostStatus.deleted # => :deleted
# Top option definition is the default
PostStatus.default # => :draftInteger representation is available as well:
PostStatus.draft_value # => 0
PostStatus.published_value # => 1
PostStatus.moderated_value # => 2
PostStatus.deleted_value # => 3Say, you need to update multiple attributes for a set of DB records with a single query:
class User < ApplicationRecord
has_many :posts
# ...
end
user = User.first
if user.nasty_spammer?
user.posts.update_all(
status: PostStatus.moderated,
moderated_by: current_user,
moderation_reason: 'being a nasty spammer'
)
endAnother example is a state machine definition with AASM gem. Here is the Post model, complemented with state transitions logic:
class Post < ApplicationRecord
include AASM
enum status: PostStatus
aasm column: :status, enum: true do
state PostStatus.draft, initial: true
state PostStatus.published
# ...
end
endBut let's make aasm block more compact by using Object#tap:
class Post < ApplicationRecord
include AASM
enum status: PostStatus
aasm column: :status, enum: true do
PostStatus.tap do |ps|
state ps.draft, initial: true
state ps.published
state ps.moderated
state ps.deleted
event :publish do
transitions from: ps.draft, to: ps.published
end
event :unpublish do
transitions from: ps.published, to: ps.draft
end
event :moderate do
transitions from: ps.published, to: ps.moderated
end
event :soft_delete do
transitions from: ps.keys.without(:deleted), to: ps.deleted
end
end
end
endNotice that soft_delete event uses PostStatus.keys shortcut, instead of declaring a separate transition for each post status.
Now the Post#state field has a set of transition rules:
post = Post.create!
post.status # => "draft"
post.publish! # perform sample transition and persist the new status
post.status # => "published"
post.soft_delete!
post.status # => "deleted"
post.moderate! # will raise an AASM::InvalidTransition, because deleted
# posts are not supposed to be moderatedInheriting enumerations
Enu descendants are immutable. In other words, after a class is declared, there is no way to change it at runtime. Use inheritance to add more options:
class AdvancedPostStatus < PostStatus
option :pinned
option :featured
endComplemented enum hash will look like this:
{
:draft => 0,
:published => 1,
:moderated => 2,
:deleted => 3,
:pinned => 4,
:featured => 5
}Tracking enums
Scoped constants help to look up enum type references in the code. Searching an enum class name or a specific value (i.e. PostStatus.draft) is a more efficient approach to navigation through the code, comparing to a situation with plain string literals or symbols, like in the Rails Guides examples. Chances are that search results for "draft" will be much noisier in a larger codebase.
Structuring type definitions
Notice that post_status.rb is located under app/enums/ subdirectory. Keeping enum classes in a separate location is not mandatory. Though, it will help to keep the project structure organized. Rails autoload mechanism will resolve all constants in app subdirectories, so there is no need to worry about requiring anything manually.
Namespaces
PostStatus example class is defined in the global namespace for the sake of brevity. In a larger real-life project it is worth considering to organize sibling classes with a module, instead of polluting root namespace:
# app/enums/post_status.rb
module Enums
class PostStatus < Enu
# ...
end
endDefault Rails autoload configuration will successfully resolve Enums::PostStatus as well.
Spring gotcha
There is a known issue with using custom directories in a Rails application. If you running your app with Spring preloader (which is true for default configuration), make sure to restart the preloader. Otherwise, Rails autoload will not resolve new constants under app/enums/ or any other custom paths, and will keep raising NameError. This command will help:
> bin/spring stop
Spring stopped.Export enum definition to JavaScript
Sometimes it is necessary to use the same enum values on the client-side, for instance, when you receive data object serializations from API. In this case, it is possible to share your enum type definition with JavaScript code.
Webpack
Make sure you have rails-erb-loader installed and enabled so your Webpack configuration will process ERB properly. If you use Webpacker gem, run bundle exec rails webpacker:install:erb.
This example will export PostStatus enum to a JavaScript object:
// app/javascript/src/enums.js.erb
export const postStatus = Object.freeze(<%= PostStatus.to_json %>)to_json method generates JSON replresentation for the enum class options:
{
"draft": "draft",
"published": "published",
"moderated": "moderated",
"deleted": "deleted"
}
Use generated object to reference enum values:
import { postStatus } from 'enums'
postStatus.draft // => "draft"Object.freeze() call in the example above will prevent accidental change of the postStatus values. It is optional, though.
Rails Asset Pipeline
Same idea:
// app/assets/javascripts/application.js
//= require_self
//= require_tree ./shared
window.App || (window.App = {});// app/assets/javascripts/shared/enums.js.erb
App.postStatus = Object.freeze(<%= PostStatus.to_json %>)Development
After checking out the repository, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/dreikanter/enu. Before sending a PR, please make sure your changes are on a new branch forked from dev, and the test coverage is kept at 100%.
License
The gem is available as open source under the terms of the MIT License.