Enums as constants in Ruby DSL
Many Rails projects have a “constant” list of some predefined things, such as user roles, food categories, types of apartments, etc. These things are usually called “enums”.
In the beginning, people prefer some easy implementation for that. For example, someone can use symbols, like
:leader all over the application code.
And that’s perfectly fine… until the time comes for changes. The business decides to rename
On try applying this change in the code the initial implementation might seem not so straightforward.
And the problem is that it’s not enough just to rename
:superadmin in all places.
:admin can be used in different contexts and might mean not a user role at all.
It might be something else, i.e. scope of a controller, or model, or … you get the point, right?
If someone goes forward and does the rename, the whole application should be tested manually. I don’t think, there is someone in the world would be happy doing that, don’t you?
What to do then?
I suggest to define constants for these things called “enums” and keep them in their namespaces. Check out the following Ruby snippet:
module HasEnumConstants class ConstantsBuilder def initialize(namespace, const) @namespace = namespace @collection = @namespace.const_get(const) end def constant(name) val = name.to_s.downcase @collection.push(val) @namespace.const_set(name, val) end end # Introduces DSL for constants definition. # The all defined contants are put into the `collection` constant. # # Usage example: # class User # extend HasEnumConstants # # constants_group :KINDS do # constant :ADMIN # constant :GUEST # end # end # User::KINDS # => ['admin', 'guest'] # User::ADMIN # => 'admin' # User::GUEST # => 'guest' def constants_group(collection, &block) const_set(collection, ) ConstantsBuilder.new(self, collection).instance_eval(&block) const_get(collection).freeze end end
If consume it by
ApplicationRecord like below all models obtain the DSL that allows enums definition as constants:
class ApplicationRecord extend HasEnumConstants end
For example, the former
:admin key could be defined on
User model like this:
class User < ApplicationRecord constants_group :KINDS do constant :ADMIN constant :GUEST end end
Feel how it works:
> User::KINDS # => ['admin', 'guest'] > User::ADMIN # => 'admin' > User::GUEST # => 'guest'
If apply this practice, all the code refers to
:admin should refer to constant
User::ADMIN. Now that name is
pretty unique as you see (because it’s scoped by
User). The chance this thing may mean something else is minimized.
Consider there is a misspelling in the code, i.e.
User::AMDIN instead of
If the code has good coverage, the code will fail immediately during the unit tests run with an adequate exception.
Having that exception it’s very easy to understand what’s the problem.
And this is why it’s better than the built-in ActiveRecord enums. ActiveRecord enums are weaker in terms of strong types and preventing mistakes during the development process.
Basically, it’s not much better than having just a bunch of not related symbols,
like in the example of this post beginning.
As one might notice, the module can be used by the other layers of a pure Ruby or Rails application, i.e. controllers, services, mailers, etc.
As practice shows, if it comes to release in production and maintenance it’s better to follow a practice like that as sooner as better.