Enumbler
Enums
are terrific, but they lack integrity. Enumbler! The enum enabler! The goal is to allow one to maintain a true foreign_key database driven relationship that also behaves a little bit like an enum
. Best of both worlds? We hope so.
Installation
Add this line to your application's Gemfile:
gem 'enumbler'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install enumbler
Usage
Suppose you have a House
and you want to add some colors
to the house. You are tempted to use an enum
but the Enumbler
is calling!
ActiveRecord::Schema.define do
create_table :colors do |t|
t.string :label, null: false, index: { unique: true }
t.string :hex, null: true
end
create_table :houses do |t|
t.references :color, foreign_key: true, null: false
end
end
class ApplicationRecord < ActiveRecord::Base
include Enumbler
self.abstract_class = true
end
# Our Color has been Enumbled with some basic colors.
class Color < ApplicationRecord
include Enumbler::Enabler
enumble :black, 1, hex: '000000'
enumble :white, 2, hex: 'ffffff'
enumble :dark_brown, 3
enumble :infinity, 4, label: 'Infinity - and beyond!'
end
# Our House class, it has a color of course!
class House < ApplicationRecord
enumbled_to :color
end
# This gives you some power:
Color::BLACK # => 1
Color.black # => equivalent to Color.find(1)
Color.black.black? # => true
Color.black.is_black # => true
Color.white.not_black? # => true
Color.all.any_black? # => true
Color.where.not(id: Color::BLACK).any_black? # => false
# Get attributes without hitting the database
Color.black(:id) # => 1
Color.black(:enum) # => :black
Color.black(:label) # => 'black'
Color.black(:graphql_enum) # => 'BLACK'
# Get an Enumble object from an id, label, enum, or ActiveRecord model
Color.find_enumbles(:black, 'white') # => [Enumbler::Enumble<:black>, Enumbler::Enumble<:white>]
Color.find_enumbles(:black, 'does-not-exist') # => [Enumbler::Enumble<:black>, nil]
Color.find_enumble(:black) # => Enumbler::Enumble<:black>
# Is pretty flexible with findidng things from strings
Color.find_enumble('Dark_Brown') # => Enumbler::Enumble<:dark-brown>
# raises errors if none found
Color.find_enumbles!(:black, 'does-no-exist') # => raises Enumbler::Error
Color.find_enumble!(:does_not_exist) # => raises Enumbler::Error
# Get ids flexibly, without raising an error if none found
Color.ids_from_enumbler(:black, 'white') # => [1, 2]
Color.ids_from_enumbler(:black, 'does-no-exist') # => [1, nil]
# Raise an error if none found
Color.ids_from_enumbler!(:black, 'does-no-exist') # => raises Enumbler::Error
Color.id_from_enumbler!(:does_not_exist) # => raises Enumbler::Error
# Get a model instance (like `.find_by` in Rails)
Color.find_by_enumble(1)
Color.find_by_enumble(:black)
Color.find_by_enumble("black")
Color.find_by_enumble("BLACK")
Color.find_by_enumble(Color.black) # => self
Color.find_by_enumble("whoops") # => nil
# Raise ActiveRecord::RecordNotFound error with bang
Color.find_by_enumble!("whoops") # => nil
# Get enumble object by id
house = House.create!(color: Color.black)
# These are all db-free lookups
house.color_label # => 'black'
house.color_enum # => :black
house.color_graphql_enum # => 'BLACK'
house.black? # => true
house.not_black? # => false
house2 = House.create!(color: Color.white)
House.color(:black, :white) # => ActiveRecord::Relation<house, house2>
House.color(Color.black, :white) # => ActiveRecord::Relation<house, house2>
Use a column other than label
By default, the Enumbler expects a table in the database with a column label
. However, you can change this to another underlying column name. Note that the enumbler still treats it as a label
column; however it will be saved to the correct place in the database. Your model now can have its own label
separate from whatever attribute/column was
specified for Enumbler usage.
ActiveRecord::Schema.define do
create_table :feelings, force: true do |t|
t.string :emotion, null: false, index: { unique: true }
t.string :label
end
end
class Feeling < ApplicationRecord
# @!parse extend Enumbler::Enabler::ClassMethods
include Enumbler::Enabler
enumbler_label_column_name :emotion
enumble :sad, 1
enumble :happy, 2
enumble :verklempt, 3, label: "Verklempt!", emotion: 'overcome with emotion'
end
# Now the `Feeling` model can use `label` if it wants to
# and not conflict with Enumbler usage (:emotion in this case)
# .enumble.label & .emotion is used internally by Enumbler
Feeling.verklempt.label # => 'Verklempt!'
Feeling.verklempt.enumble.label # => 'overcome with emotion'
Feeling.verklempt.emotion # => 'overcome with emotion'
Core ext
Adds case equality power to the Symbol
class allowing you to use case methods directly against an enabled instance:
case Color.black
when :black
'not surprised'
when :blue, :purple
'very surprised'
end
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
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.
Matrix testing
This gem supporst different versions of Ruby and Rails. To test against all supported versions, run:
BUNDLE_GEMFILE=Gemfile.rails7.2 bundle install
BUNDLE_GEMFILE=Gemfile.rails7.2 bundle exec rspec
Roadmap
- Ideally, we could make this work more like a traditional
enum
; for example, overriding the.where
method by allowing something like:House.where(color: :blue)
instead ofHouse.color(:blue)
. But right now am in a rush and not sure how to go about doing that properly.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/linguabee/enumbler.
License
The gem is available as open source under the terms of the MIT License.