Power Enum¶ ↑
github.com/albertosaurus/power_enum_2
Enumerations for Rails Done Right.
Versions¶ ↑
-
PowerEnum
4.0.X (this version) supports Rails 6.X, and Rails 7.0 (Experimental) -
PowerEnum
3.X supports Rails 4.2, Rails 5.X and Rails 6.0 -
PowerEnum
2.X supports Rails 4.X and Rails 5.0 -
PowerEnum
1.X supports Rails 3.1/3.2, available here: github.com/albertosaurus/power_enum
What is this?:¶ ↑
Power Enum allows you to treat instances of your ActiveRecord
models as though they were an enumeration of values. It allows you to cleanly solve many of the problems that the traditional Rails alternatives handle poorly if at all. It is particularly suitable for scenarios where your Rails application is not the only user of the database, such as when it’s used for analytics or reporting.
Power Enum is a fork of the Rails 3 modernization made by the fine folks at Protocool github.com/protocool/enumerations_mixin to the original plugin by Trevor Squires. While many of the core ideas remain, it has been reworked and a full test suite written to facilitate further development.
At it’s most basic level, it allows you to say things along the lines of:
# Create a provisional booking booking = Booking.new( status: BookingStatus[:provisional] ) # This also works booking = Booking.new( status: :provisional ) # Set the booking status to 'confirmed' booking.status = :confirmed booking = Booking.create( status: :rejected ) # And now... booking.status == BookingStatus[:rejected] # evaluates to true booking.status === :rejected # also evaluates to true booking.status === [:rejected, :confirmed, :provisional] # and so does this booking.status === [%i[rejected confirmed provisional]] # and this Booking.where( :status_id => BookingStatus[:provisional] ) BookingStatus.all.collect { |status|, [status.name, status.id] } # built in scopes make life easier Booking.with_status( :provisional, :confirmed )
See “How to use it” below for more information.
Requirements¶ ↑
PowerEnum
4.0.X¶ ↑
-
Ruby 2.7 or later (JRuby should work but isn’t extensively tested).
-
Rails 6.0, 6.1, 6.2, 7.0
PowerEnum
3.X¶ ↑
-
Ruby 2.1 or later (JRuby should work but isn’t extensively tested).
-
Rails 4.2, 5.0, 5.1, 5.2, 6.0
PowerEnum
2.X¶ ↑
-
Ruby 1.9.3, 2.0, JRuby 1.7+ (Ruby 1.9.3 or 2.0 required for development)
-
Rails 4.0, 4.1, 4.2, 5.0
Installation¶ ↑
Using Bundler¶ ↑
Add the gem to your Gemfile
gem 'power_enum'
then run
bundle install
Manual Installation¶ ↑
gem install power_enum
Gem Contents¶ ↑
This package adds: - Two mixins and a helper to ActiveRecord
- Methods to migrations to simplify the creation of backing tables - Two generators to streamline working with enums - Custom RSpec matchers to streamline the testing of enums and enumerated attributes
acts_as_enumerated
provides capabilities to treat your model and its records as an enumeration. At a minimum, the database table for an acts_as_enumerated must contain an ‘id’ column and a column to hold the value of the enum (‘name’ by default). It is strongly recommended that there be a NOT NULL constraint on the ‘name’ column. All instances for the acts_as_enumerated
model are cached in memory. If the table has an ‘active’ column, the value of that attribute will be used to determine which enum instances are active. Otherwise, all values are considered active.
has_enumerated
adds methods to your ActiveRecord
model for setting and retrieving enumerated values using an associated acts_as_enumerated model.
There is also an ActiveRecord::VirtualEnumerations
helper module to create ‘virtual’ acts_as_enumerated models which helps to avoid cluttering up your models directory with acts_as_enumerated classes.
How to use it¶ ↑
In the following example, we’ll look at a Booking that can have several types of statuses, encapsulated by BookingStatus enums.
generator¶ ↑
Invoke the generator to create a basic enum:
rails generate enum booking_status
You should see output similar to this:
create app/models/booking_status.rb create db/migrate/20110926012928_create_enum_booking_status.rb invoke test_unit create test/unit/booking_status_test.rb
That’s all you need to get started. In many cases, no further work on the enum is necessary. You can run rails generate enum --help
to see a description of the generator options. Notice, that while a unit test is generated by default, a fixture isn’t. That is because fixtures are not an ideal way to test acts_as_enumerated models. I generally prefer having a hook to seed the database from seeds.rb from a pre-test Rake task.
migration¶ ↑
When you open your migration file, it will look something like this:
class CreateEnumBookingStatus < ActiveRecord::Migration def change create_enum :booking_status end end
You can now customize it.
create_enum :booking_status, :name_limit => 50 # The above is equivalent to saying # create_table :booking_statuses do |t| # t.string :name, :limit => 50, :null => false # end
WARNING - This conflicts with PostgreSQL enum support in Rails 7+ and will be renamed in future versions.
Now, when you create your Booking model, your migration should create a reference column for status id’s and a foreign key relationship to the booking_statuses table.
create_table :bookings do |t| t.integer :status_id t.timestamps end # It's highly recommended to add a foreign key constraint here. # Ideally, you would use a gem of some sort to handle this for Rails < 6. # I have been using PgPower https://rubygems.org/gems/pg_power with much # success. It's fork, PgSaurus https://rubygems.org/gems/pg_saurus should # work just as well. execute "ALTER TABLE bookings ADD 'bookings_bookings_status_id_fk'"\ " FOREIGN KEY (status_id) REFERENCES booking_statuses (id);"
It’s easier to use the references
method if you intend to stick to the default naming convention for reference columns.
create_table :bookings do |t| t.references :booking_status # Same as t.integer booking_status_id t.timestamps end
There are two methods added to Rails migrations:
create_enum(enum_name, options = {}, &block)¶ ↑
WARNING - This conflicts with PostgreSQL enum support in Rails 7+ and will be renamed in future versions.
Creates a new enum table. enum_name
will be automatically pluralized. The following options are supported:
-
[:name_column] Specify the column name for name of the enum. By default it’s :name. This can be a String or a Symbol
-
[:description] Set this to
true
to have a ‘description’ column generated. -
[:name_limit] Set this define the limit of the name column.
-
[:desc_limit] Set this to define the limit of the description column
-
[:active] Set this to
true
to have a boolean ‘active’ column generated. The ‘active’ column will have the options of NOT NULL and DEFAULT TRUE. -
[:timestamps] Set this to
true
to have the timestamp columns (created_at and updated_at) generated -
[:table_options] Allows you to set a hash which will be passed directly to
create_table
. -
[:schema] Allows you to create the enum table in a different schema (Version 2.6.0).
You can also pass in a block that takes a table object as an argument, like create_table
.
Example:
create_enum :booking_status
is the equivalent of
create_table :booking_statuses do |t| t.string :name, :null => false end add_index :booking_statuses, [:name], :unique => true
In a more complex case:
create_enum :booking_status, :name_column => :booking_name, :name_limit => 50, :description => true, :desc_limit => 100, :active => true, :timestamps => true, :table_options => {:primary_key => :foo}
is the equivalent of
create_table :booking_statuses, :primary_key => :foo do |t| t.string :booking_name, :limit => 50, :null => false t.string :description, :limit => 100 t.boolean :active, :null => false, :default => true t.timestamps end add_index :booking_statuses, [:booking_name], :unique => true
You can also customize the creation process by using a block:
create_enum :booking_status do |t| t.boolean :first_booking, :null => false end
is the equivalent of
create_table :booking_statuses do |t| t.string :name, :null => false t.boolean :first_booking, :null => false end add_index :booking_statuses, [:name], :unique => true
Notice that a unique index is automatically created on the specified name column.
remove_enum(enum_name)¶ ↑
Drops the enum table. enum_name
will be automatically pluralized.
Example:
remove_enum :booking_status
is the equivalent of
drop_table :booking_statuses
acts_as_enumerated¶ ↑
class BookingStatus < ActiveRecord::Base acts_as_enumerated :conditions => 'optional_sql_conditions', :order => 'optional_sql_order_by', :on_lookup_failure => :optional_class_method, # This also works: lambda{ |arg| some_custom_action } :name_column => 'optional_name_column' # If required, may override the default name column :alias_name => false, # If set to false and have name_column set, will not # alias :name to the name column attribute. :freeze_members => true # Optional, default is true in prod. # This also works: lambda { true } end
With that, your BookingStatus class will have the following methods defined:
Class Methods¶ ↑
¶ ↑
BookingStatus[arg]
performs a lookup for the BookingStatus instance for the given arg. The arg value can be a ‘string’ or a :symbol, in which case the lookup will be against the BookingStatus.name field. Alternatively arg can be a Integer, in which case the lookup will be against the BookingStatus.id field. It returns the arg if arg is an instance of the enum (in this case BookingStatus) as a convenience.
The :on_lookup_failure
option specifies the name of a class method to invoke when the []
method is unable to locate a BookingStatus record for arg. The default is the built-in :enforce_none
which returns nil. There are also built-ins for :enforce_strict
(raise and exception regardless of the type for arg), :enforce_strict_literals
(raises an exception if the arg is a Integer or Symbol), :enforce_strict_ids
(raises and exception if the arg is a Integer) and :enforce_strict_symbols
(raises an exception if the arg is a Symbol).
The purpose of the :on_lookup_failure
option is that a) under some circumstances a lookup failure is a Bad Thing and action should be taken, therefore b) a fallback action should be easily configurable. You can also set :on_lookup_failure
to a lambda that takes in a single argument (The arg that was passed to []
).
You can also pass in multiple arguments to []
. This returns a list of enums corresponding to the passed in values. Duplicates are filtered out. For example BookingStatus[arg1, arg2, arg3]
would be equivalent to [BookingStatus[arg1], BookingStatus[arg2], BookingStatus[arg3]]
.
contains?(arg)¶ ↑
BookingStatus.contains?(arg)
returns true if
the given Symbol, String or id has a member instance in the enumeration, false
otherwise. Returns true
if the argument is an enum instance, returns false
if the argument is nil
or any other value.
all¶ ↑
BookingStatus.all
returns an array of all BookingStatus records that match the :conditions
specified in acts_as_enumerated
, in the order specified by :order
.
all_except(*items)¶ ↑
BookingStatus.all_except(arg1, arg2)
returns an array of all BookingStatus records with the given items filtered out.
active¶ ↑
BookingStatus.active
returns an array of all BookingStatus records that are marked active. See the active?
instance method.
inactive¶ ↑
BookingStatus.inactive
returns an array of all BookingStatus records that are inactive. See the inactive?
instance method.
names¶ ↑
BookingStatus.names
will return all the names of the defined enums as an array of symbols.
update_enumerations_model¶ ↑
The preferred mechanism to update an enumerations model in migrations and similar. Pass in a block to this method to to perform any updates.
Example:
BookingStatus.update_enumerations_model do BookingStatus.create :name => 'Foo', :description => 'Bar', :active => false end
Example 2:
BookingStatus.update_enumerations_model do |klass| klass.create :name => 'Foo', :description => 'Bar', :active => false end
acts_as_enumerated?¶ ↑
Returns true
for ActiveRecord
models that act as enumerated, false
for others. So BookingStatus.acts_as_enumerated?
would return true
, while Booking.acts_as_enumerated?
would return false
.
Instance Methods¶ ↑
Each enumeration model gets the following instance methods.
===(arg)¶ ↑
Behavior depends on the type of arg
.
-
If
arg
isnil
, returnsfalse
. -
If
arg
is an instance ofSymbol
,Integer
orString
, returns the result ofBookingStatus[:foo] == BookingStatus[arg]
. -
If
arg
is anArray
, returnstrue
if any member of the array returnstrue
for===(arg)
,false
otherwise. -
In all other cases, delegates to
===(arg)
of the superclass.
Examples:
BookingStatus[:foo] === :foo #Returns true BookingStatus[:foo] === 'foo' #Returns true BookingStatus[:foo] === :bar #Returns false BookingStatus[:foo] === [:foo, :bar, :baz] #Returns true BookingStatus[:foo] === nil #Returns false
You should note that defining an :on_lookup_failure
method that raises an exception will cause ===
to also raise an exception for any lookup failure of BookingStatus[arg]
.
like?(arg)¶ ↑
Aliased to ===
in?(*list)¶ ↑
Returns true if any element in the list returns true for ===(arg)
, false otherwise.
Example:
BookingStatus[:foo].in? :foo, :bar, :baz #Returns true
to_s¶ ↑
Returns the string representation of the enum, i.e. the value in the :name_column
attribute of the enumeration model.
name¶ ↑
By default, aliased to the string representation of the :name_column
attribute. To avoid this, set the alias_name
option to false
.
name_sym¶ ↑
Returns the symbol representation of the name of the enum. BookingStatus[:foo].name_sym
returns :foo.
to_sym¶ ↑
Aliased to name_sym
.
active?¶ ↑
Returns true if the instance is active, false otherwise. If it has an attribute ‘active’, returns the attribute cast to a boolean, otherwise returns true. This method is used by the active
class method to select active enums.
inactive?¶ ↑
Returns true if the instance is inactive, false otherwise. Default implementations returns !active?
This method is used by the inactive
class method to select inactive enums.
Notes¶ ↑
acts_as_enumerated
records are considered immutable. By default you cannot create/alter/destroy instances because they are cached in memory. Because of Rails’ process-based model it is not safe to allow updating acts_as_enumerated records as the caches will get out of sync. Also, to_s
is overriden to return the name of the enum instance.
However, one instance where updating the models should be allowed is if you are using seeds.rb to seed initial values into the database.
Using the above example you would do the following:
BookingStatus.enumeration_model_updates_permitted = true ['pending', 'confirmed', 'canceled'].each do | status_name | BookingStatus.create( :name => status_name ) end
Note that a :presence
and :uniqueness
validation is automatically defined on each model for the name column.
has_enumerated¶ ↑
First of all, note that you could specify the relationship to an acts_as_enumerated
class using the belongs_to
association. However, has_enumerated
is preferable because you aren’t really associated to the enumerated value, you are aggregating it. As such, the has_enumerated
macro behaves more like an aggregation than an association.
class Booking < ActiveRecord::Base has_enumerated :status, :class_name => 'BookingStatus', :foreign_key => 'status_id', :on_lookup_failure => :optional_instance_method, :permit_empty_name => true, #Setting this to true disables automatic conversion of empty strings to nil. Default is false. :default => :unconfirmed, #Default value of the attribute. :create_scope => false #Setting this to false disables the automatic creation of the 'with_status' scope. end
By default, the foreign key is interpreted to be the name of your has_enumerated field (in this case ‘booking_status’) plus ‘_id’. Since we chose to make the column name ‘status_id’ for the sake of brevity, we must explicitly designate it. Additionally, the default value for :class_name
is the camelized version of the name for your has_enumerated field. :on_lookup_failure
is explained below. :permit_empty_name
is an optional flag to disable automatic conversion of empty strings to nil. It is typically desirable to have booking.update_attributes(:status => '')
assign status_id to a nil rather than raise an Error, as you’ll be often calling update_attributes
with form data, but the choice is yours. Setting a :default
option will generate an after_initialize callback to set a default value on the attribute unless a non-nil value has already been set.
With that, your Booking class will have the following methods defined:
status¶ ↑
Returns the BookingStatus with an id that matches the value in the Booking.status_id.
status=(arg)¶ ↑
Sets the value for Booking.status_id using the id of the BookingStatus instance passed as an argument. As a short-hand, you can also pass it the ‘name’ of a BookingStatus instance, either as a ‘string’ or :symbol, or pass in the id directly.
example:
mybooking.status = :confirmed
this is equivalent to:
mybooking.status = 'confirmed'
or:
mybooking.status = BookingStatus[:confirmed]
The :on_lookup_failure
option in has_enumerated is there because you may want to create an error handler for situations where the argument passed to status=(arg)
is invalid. By default, an invalid value will cause an ArgumentError to be raised.
Of course, this may not be optimal in your situation. In this case you can do one of three things:
1) You can set it to ‘validation_error’. In this case, the invalid value will be cached and returned on subsequent lookups, but the model will fail validation.
2) Specify an instance method to be called in the case of a lookup failure. The method signature is as follows:
your_lookup_handler(operation, name, name_foreign_key, acts_enumerated_class_name, lookup_value)
The ‘operation’ arg will be either :read
or :write
. In the case of :read
you are expected to return something or raise an exception, while in the case of a :write
you don’t have to return anything.
Note that there’s enough information in the method signature that you can specify one method to handle all lookup failures for all has_enumerated fields if you happen to have more than one defined in your model.
3) Give it a lambda function. In that case, the lambda needs to accept the ActiveRecord
model as its first argument, with the rest of the arguments being identical to the signature of the lookup handler instance method.
:on_lookup_failure => lambda{ |record, op, attr, fk, cl_name, value| # handle lookup failure }
NOTE: A nil
is always considered to be a valid value for status=(arg)
since it’s assumed you’re trying to null out the foreign key. The :on_lookup_failure
will be bypassed.
with_enumerated_attribute scope¶ ↑
Unless the :create_scope
option is set to false
, a scope is automatically created that takes a list of enums as arguments. This allows us to say things like:
Booking.with_status :confirmed, :received
Strings, symbols, ids, or enum instances are all valid arguments. For example, the following would be valid, though not recommended for obvious reasons.
Booking.with_status 1, 'confirmed', BookingStatus[:rejected]
As a convenience, it also aliases a pluralized version of the scope, i.e. :with_statuses
exclude_enumerated_attribute scope¶ ↑
By default, a scope for the inverse of with_enumerated_attribute
is created, unless the :create_scope
option is set to false
. As a result, this allows us to do things like
Booking.exclude_status :received
This will give us all the Bookings where the status is a value other than BookingStatus[:received]
.
NOTE: This will NOT pick up instances of Booking where status is nil.
A pluralized version of the scope is also created, so Booking.exclude_statuses :received, :confirmed
is valid.
ActiveRecord::Base Extensions¶ ↑
The following methods are added to ActiveRecord::Base as class methods.
has_enumerated?(attr)¶ ↑
Returns true if the given attr is an enumerated attributes, false otherwise. attr
can be a string or a symbol. This is a class method.
enumerated_attributes¶ ↑
Returns an array of attributes which are enumerated.
ActiveRecord::VirtualEnumerations
¶ ↑
In many instances, your acts_as_enumerated
classes will do nothing more than just act as enumerated. In that case, you can use ActiveRecord::VirtualEnumerations
to reduce that clutter.
Create a custom Rails initializer: Rails.root/config/initializers/virtual_enumerations.rb
To streamline this, a generator is provided:
rails generate virtual_enumerations_initializer
Configure as appropriate.
ActiveRecord::VirtualEnumerations.define do |config| # Define the enum class config.define 'ClassName', :table_name => 'table', :extends => 'SuperclassName', :conditions => ['something = ?', "value"], :order => 'column ASC', :on_lookup_failure => :enforce_strict, :name_column => 'name_column', :alias_name => false { # This gets evaluated within the class scope of the enum class. def to_s "#{id} - #{name}" end } end
Only the ‘ClassName’ argument is required. :table_name
is used to define a custom table name while the :extends
option is used to set a custom superclass. Class names can be either camel-cased like ClassName or with underscores, like class_name. Strings and symbols are both fine.
If you need to fine-tune the definition of the enum class, you can optionally pass in a block, which will be evaluated in the context of the enum class.
Example:
config.define :color, :on_lookup_failure => :enforce_strict, do def to_argb(alpha) case self.to_sym when :white [alpha, 255, 255, 255] when :red [alpha, 255, 0, 0] when :blue [alpha, 0, 0, 255] when :yellow [alpha, 255, 255, 0] when :black [alpha, 0, 0, 0] end end end
As a convenience, if multiple enums share the same configuration, you can pass all of them to config.define.
config.define :booking_status, :connector_type, :color, :order => :name
STI is also supported:
config.define :base_enum, :name_column => ;foo config.define :booking_status, :connector_type, :color, :extends => :base_enum
Testing¶ ↑
A pair of custom RSpec matchers are included to streamline testing of enums and enumerated attributes.
act_as_enumerated¶ ↑
This is used to test that a model acts as enumerated. Example:
describe BookingStatus do it { should act_as_enumerated } end
This also works:
describe BookingStatus do it "should act as enumerated" do BookingStatus.should act_as_enumerated end end
You can use the with_items
chained matcher to test that each enum is properly seeded:
describe BookingStatus do it { should act_as_enumerated.with_items(:confirmed, :received, :rejected) } end
You can also pass in hashes if you want to be thorough and test out all the attributes of each enum. If you do this, you must pass in the :name
attribute in each hash
describe BookingStatus do it { should act_as_enumerated.with_items( { :name => 'confirmed', :description => "Processed and confirmed" }, { :name => 'received', :description => "Pending confirmation" }, { :name => 'rejected', :description => "Rejected due to internal rules" } ) } end
have_enumerated¶ ↑
This is used to test that a model has enumerated the given attribute:
describe Booking do it { should have_enumerated(:status) } end
This is also valid:
describe Booking do it "Should have enumerated the status attribute" do Booking.should have_enumerated(:status) end end
match_enum¶ ↑
Tests if an enum instance matches the given value, which may be a symbol, id, string, or enum instance:
describe Booking do it "status should be 'received' for a new booking" do Booking.new.status.should match_enum(:received) end end
Of course Booking.new.status.should === :received
still works, but is liable to produce false positives.
How to run tests¶ ↑
Prepare the test database¶ ↑
Automatically (preferred)¶ ↑
Execute the test setup script:
script/test_setup.sh
Manually (if required)¶ ↑
Go to the ‘dummy’ project:
cd ./spec/dummy
If this is your first time, create the test database
RAILS_ENV=test bundle exec rake db:create
Run migrations for test environment:
RAILS_ENV=test bundle exec rake db:migrate
Go back to gem root directory:
cd ../../
Run tests¶ ↑
bundle exec rake spec
Copyrights and License¶ ↑
-
Initial Version Copyright © 2005 Trevor Squires
-
Rails 3 Updates Copyright © 2010 Pivotal Labs
-
Initial Test Suite Copyright © 2011 Sergey Potapov
-
Subsequent Updates Copyright © 2011-2020 Arthur Shagall
Released under the MIT License. See the LICENSE file for more details.
Contributing¶ ↑
Contributions are welcome. However, please make sure of the following before issuing a pull request:
-
All specs are passing.
-
Any new features have test coverage. Use the SimpleCov report to confirm.
-
Anything that breaks backward compatibility has a very good reason for doing so.