Using a bitmask for options in Rails
Sometimes I like storing a set of options for a model as in integer in the DB, but still want a nice interface to use those options in code. This is one approach I've been using.
Sometimes I like storing a set of options for a model as in integer in the DB, but still want a nice interface to use those options in code. This is one approach I've been using.
In this case the bitmask is call capabilities_mask
, but could be whatever.
Database
class AddCapabilitiesToPhoneNumbers < ActiveRecord::Migration[5.2]
def change
add_column :phone_numbers, :capabilities_mask, :integer, null: false, default: 0
end
end
Model
This is the bulk of the code.
First we define the const with the values. This can be added do, but the order shouldn't change over time. Also, I'm using integers for the bitmasks, so they need to double for each new value.
Next we add methods to check for the value, dtmf?
, for example will return true if the mask value contains dtmf
.
And dtmf!
will add that option to the mask.
The capabilities
setter & getter are mostly for display and updating from form, though they can be used for anywhere.
class PhoneNumber
...
CAPABILITIES = {
dtmf: 1,
text_to_speech: 2,
agent_text: 4,
agent_voice: 8,
}.with_indifferent_access
CAPABILITIES.each do |cap, bit|
define_method("#{cap}?") do
capabilities_mask & bit == bit
end
define_method("#{cap}!") do
self.capabilities_mask |= bit
end
end
def capabilities
CAPABILITIES.filter { |cap, bit| self.capabilities_mask & bit == bit }.map { |cap, bit| cap }
end
def capabilities= caps
self.capabilities_mask = Array.wrap(caps).filter(&:present?).map { |cap| CAPABILITIES[cap] }.reduce(0) { |m, b| m |= b }
end
...
end
Controllers
Just need to allow the params since the model will handle setting the mask value.
params.require(:phone_number).permit(:number, :name, :status, capabilities: [])
Views
I use SimpleForm in most of my projects, so here's how to display a list of check boxes.
...
= f.input :capabilities, as: :check_boxes, collection: PhoneNumber::CAPABILITIES.map{|k,v| [k.to_s.humanize,k]}
...
In the show view.
p
strong Capabilities:
=< @phone_number.capabilities.map{|k, v| k.to_s.humanize}.join(", ")
Upsides / Downsides
Storing options this way only takes an interger column in the datbase, and potentially in API's. I usually use the text in API's though.
The biggest downside is the data isn't obvious in the DB and can be a pain to figure out an option, for example 11 would be dtmf, text_to_speech, and agent_voice in this example.
Webmentions
These are webmentions via the IndieWeb and webmention.io. Mention this post from your site: