Dec 18, 2018

Using ActiveModel::Attributes

In one of my projects, we use service objects for things that are otherwise uncomfortable with the ActiveRecord pattern. There are many reasons why something might be uncomfortable with the ActiveRecord pattern, but the most frequently seen reason is for an attribute that isn’t stored, but used to calculate another stored attribute. We’ll look at what ActiveModel::Attributes is, and what we can do with it.

How ActiveModel::Attributes works

ActiveModel::Attributes is used under the hood of ActiveRecord (your database driver) to cast data from your database into Ruby objects. Example: If you’re storing decimals in postgres, you need to cast them to BigDecimal. ActiveModel is what handles this under the hood.

ActiveRecord is smart enough to know what columns the model’s table has, so it creates getters and setters on behalf of the database so you can use familiar Ruby idioms for your database. To achieve this, it uses its attribute DSL.

So, if you had a Post table with columns of user_id, body, ActiveRecord would automatically generate something like this:

class Post < ApplicationRecord
  attribute :user_id, :integer
  attribute :body, :text 
end

This allows you to do

post = Post.new 
post.user_id = "1" # assuming you're receiving params from a controller, because all params are strings
post.body = "hello"
post.user_id.class # Integer

And then, when you want to save or update an object to the database, ActiveRecord looks at the columns available to it and smartly picks those attributes for sending to your database. This means we can have attrbiutes that act like database-backed columns but aren’t.

A real-world example

All good things need a real-world example. We’ll look at how I handle credit cards for TinyVoicemail.

TinyVoicemail uses Stripe for payment processing and subscription management via Stripe Billing. It’s a great product run by great people. Using Stripe doesn’t require you to be PCI-compliant, as they allow you to generate single-use tokens that, once consumed, provide a Stripe-backed credit card object. Stripe’s implementation is worth its own post so I won’t go into too much detail.

Below is a truncated version of the CreditCard model used by TinyVoicemail (and an upcoming open-source SaaS framework!). We’ll go through the relevant goodies that’s provided by ActiveModel to create Stripe-backed credit cards.

class CreditCard < ApplicationRecord
  belongs_to :account, inverse_of: :credit_cards

  attribute :token, :string 

  validates_uniqueness_of :stripe_card_id
  validates_presence_of :last4, 
                        :exp_mo, 
                        :exp_year, 
                        :stripe_card_id

  before_validation :create_stripe_card!, on: [:create]

  private

  def as_stripe_card
    @as_stripe_card ||= customer.sources.retrieve(stripe_card_id)
  end

  def customer
    @customer ||= account.as_stripe_customer
  end

  def create_stripe_card!
    stripe_request = customer.sources.create(source: token)
    self.last4 = stripe_request.last4
    self.kind = stripe_request.brand
    self.exp_mo = stripe_request.exp_month
    self.exp_year = stripe_request.exp_year
    self.stripe_card_id = stripe_request.id
  rescue Stripe::StripeError => e
    errors.add(:base, e.message)
    throw(:abort)
  rescue StandardError => e
    errors.add(:base, e.message)
    throw(:abort)
  end
end

In the controller that handles credit cards, to create a new card, we simply (briefly),

@card = CreditCard.new(token: card_params[:token])
@card.save 

token is has a getter and setter defined on our CreditCard model via ActiveModel::Attributes. It’s not backed by the database, so we really don’t care about it on an instance-by-instance basis.

attribute :token, :string 

Then, we use it do set attributes that we do care about before we validate the object.

before_validation :create_stripe_card!, on: [:create]

In our create_stripe_card! method, we use Stripe’s Ruby SDK to create our card.

def create_stripe_card!
  stripe_request = customer.sources.create(source: token)
  self.last4 = stripe_request.last4
  self.kind = stripe_request.brand
  self.exp_mo = stripe_request.exp_month
  self.exp_year = stripe_request.exp_year
  self.stripe_card_id = stripe_request.id
rescue Stripe::StripeError => e
  errors.add(:base, e.message)
  throw(:abort)
rescue StandardError => e
  errors.add(:base, e.message)
  throw(:abort)
end

Of note, we can access the token we defined earlier just how we would any other database-backed attribute.

stripe_request = customer.sources.create(source: token)

Inside this method, we rescue from errors and apply them as necessary to the object, applying errors to our object that act like database-backed columns too.

Assuming everything goes to plan, we’ll persist this object to our database and we’ll never see our beloved token again — because we don’t have any use for it. I personally find this approach to be a lot cleaner than using a separate class to create this object and provide feedback to users.