ruby-on-railsactiverecordactivesupport-concern

How to abstract enum attribute used by multiple models in Rails?


I have a database attribute called currency which is used by multiple models. I'd like to create a concern (or similar) which defines the basic functionality of the currency attribute. If it was only used in a single model, I'd probably do something like this:

class Transaction < ApplicationRecord
  enum currency: [ :USD, :AUD ] 
  validates :currency, inclusion: { in: currencies.keys }
end

But since it's used by multiple models, I'd like to do something like this instead:

class Transaction < ApplicationRecord
  include Currency # maybe in a Concern?
  acts_as_currency :currency
end

The whole idea is to be able to do things like Transaction.first.currency.USD? and similar functionality that an enum attribute has, as well as defining the attribute validation in a single place.

How would you design that with Rails? Is there any preferred way to do so?


Solution

  • In Ruby, shared behaviours call for modules, ActiveSupport concerns provide cool syntactic sugar, e.g. included

    # app/models/concerns/has_currency.rb
    module HasCurrency
      extend ActiveSupport::Concern
      included do
        enum currency: [ :USD, :AUD ] 
        validates :currency, inclusion: { in: currencies.keys }
      end
    end
    
    class Transaction < ApplicationRecord
      include HasCurrency
    end
    

    We can give the user of our concern more freedom by going through a function instead, as you seem to suggest:

    # app/models/concerns/has_currency.rb
    module HasCurrency
      def acts_as_currency(column = :currency)
        enum column => [ :USD, :AUD ] 
        validates column, inclusion: { in: column.to_s.pluralize.keys }
      end
    end
    
    class Transaction < ApplicationRecord
      extend HasCurrency # Note the `extend`, not `include`
      acts_as_currency
    end