Using a bitmask for options in Rails

Dusty Candland | | 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.

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: