Using Rails ActiveModel::Attributes, and practical ActiveModel examples

One of my clients has a project that makes great use of what most would agree is a service object pattern1, primarily because there’s an attribute that’s not a database attribute that needs to be sent to the business logic.

There are many reasons why you might have an attribute that’s closely-coupled to a data model, but is not otherwise stored. Some of these reasons include:

  1. Used to calculate the value of a stored attribute
  2. Used to trigger a callback
  3. Used in an external API service

Instead of spreading your business logic too thin, you can put this in your model. This post will show you how.

Contents

The data-model/business-logic argument

Models in Ruby on Rails get a bad rap. They’re often cluttered with business logic, view methods, API calls and so much more. Unfortunately, to all kinds of developers, it’s not entirely clear where to put business logic due to lack of proper guidance from the official documentation or community.

This isn’t defending fat models or service objects. There are certainly reasons when a service object pattern makes sense. However, in for the purposes of this feature, I see it as unnecessary.

My personal opinion is that if there is an attribute that is used to set other attributes, it can be argued that it fits into the model.

I’ll be honest: I don’t absolutely hate ActiveRecord callbacks. Their reputation is awful because we often try to do too much with them without looking at the tools available to us.

What is ActiveModel::Attributes

ActiveModel::Attributes is used under the hood of ActiveRecord to cast database column types to Ruby-friendly object types. Example: If you’re storing decimals in PostgreSQL, 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. You can do this yourself by using the attribute API:

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

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

Which allows you to:

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
post.body.class # string 

A real-world example

All good things need a real-world example. We’ll look at how I handle credit cards for one of my projects. This project uses Stripe for payment processing and subscription management via Stripe Billing.

Below is a truncated version of the CreditCard model used by this SaaS (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.

The model

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

  attribute :token, :string 

  validates :stripe_card_id, uniqueness: true 
  validates :last4, :exp_mo, :exp_year, :stripe_card_id, presence: true 

  before_validation :create_stripe_card!, on: [:create]

  private

  def stripe_customer
    @stripe_customer ||= user.as_stripe_customer
  end

  def create_stripe_card!
    stripe_request = stripe_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(:token, e.message)
    throw(:abort)
  rescue StandardError => e
    errors.add(:base, e.message)
    throw(:abort)
  end
end

The Controller

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

def create 
  @card = current_user.credit_cards.new(token: card_params[:token])
  if @card.save 
    flash[notice] = "Successfully added card."
  else 
    flash[:error] = @card.errors.full_messages.to_sentence
  end 
  redirect_back(fallback_location: user_credit_cards_path)
end

Breaking it down

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 = stripe_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(:token, 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.

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.

References

  1. More or less POROs that started off with good intentions but quickly became a mess of copy-pasted business logic