One more useful abstraction for Rails - My take on Form objects

Recently I heard a lot of talk about Form objects in Rails. In some of my projects I played around with this idea and I wanted to share my experience with the community.

The use case I will be discussing is a registration form example. In the application that I worked on, there was a possibility for the user to register with Facebook and via the registration form. For some reason, Facebook does not return email for some users, but we wanted to keep the registration as simple as possible for a customer. This means that user model either has to have a conditional validation or we could use Form object to deal with this situation. Enough talking, let's start doing.

Note that this app is an API only, so at this point I am not concerned about using a form.

The ideal interface

registration_service = RegistrationForm.new(user_params)
registration_service.on_success do |user|
  render json: {token: user.token}, status: 200
end
registration_service.on_error do |errors|
  render json: errors, status: 422
end
registration_service.call

Let's discuss the interface - I was playing around with callback idea for a while now and it seems to me that I have found a sweet spot. We add two different scenarios that might happen via blocks. One is on_success and the other one is on_failure. In the first case, we would return a user object and in the other, it would be an errors hash.

With the interface in place, let's think about the way we would want to implement this.

First of all I want to extract callback code so that it can be used with other form objects in my project. I would do it in following way:

module FormCallbacks
  def on_error(&block)
    @error_callback = block
  end

  def on_success(&block)
    @success_callback = block
  end

  def call
    raise NotImplementedError, "You need to implement call method in order to use FormCallbacks module"
  end

  private

  attr_reader :success_callback, :error_callback
end

With the help of this module, we are able to register our callbacks and call them in our Form object. With the help of on_success and on_error , we are storing a block in an attribute reader success_callback and error_callback. Also, we would need to implement call method in order to achieve the interface that we defined in the beginning. With that out of the way, let's see how the main part works.

class RegistrationForm
  include Virtus.model
  include ActiveModel::Model
  include FormCallbacks

  attribute :email, String
  attribute :full_name, String
  attribute :password, String
  attribute :phone_number, String

  validates :email, :full_name, :password, presence: true
  validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i}
  validates :password, length: {in: 6..50}

  def call
    if valid?
      try_persist_user
    else
      error_callback.call(errors.messages)
    end
  end

  def try_persist_user
    user = User.new(attributes)
    if user.save
      success_callback.call(user)
    else
      error_callback.call(user.errors.messages)
    end
  end
end

First of all, I am using virtus gem to define attributes for my application. It's a really nice library that can help you with attribute definition and coercion to your specified data types. Also, it will allow us to accept a constructor for attributes defined. I highly recommend you taking a look at Virtus. It is packed with a ton of useful features.
Afterwards, I am including ActiveModel::Model to have validations at my disposal. You can see how seamlessly virtus is working with ActiveModel interface.
Now let's talk about what I am doing in a call method. First, we check ourselves for validity. So if we are not valid, meaning that we have not passed our defined validations, we call error_callback with errors that have not complied with defined rules. These errors are the ones that are stored on the Form object and are given to us by ActiveModel::Model.
If we are valid, we continue to a try_persist_user method. This method in turn will try to save our user with attributes hash that is given to us by virtus. If the saving fails, we return errors of the user to our error_callback. These errors are the actual validations on our User model. Those can be specific to the data layer only, like email uniqueness validation e.t.c.
If the saving of user succeeds, we just return a persisted instance of the user to the success callback.

And it is as simple as that. In actual code, we have some other user post-processing actions, like sending a welcome email, creating the associated settings record and persisting of avatar after the user has been saved successfully.

I can relate that callbacks are not the standard way of doing things for such Form objects, but this might be an interesting idea for you to play around with. I would love to receive feedback on this.

Janis Miezitis

Read more posts by this author.

Subscribe to Janis Miezitis personal blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!