Ignacio Capuccio
Ignacio Capuccio

Reputation: 11

How to handle and dry validations in Rails controllers

I am trying to find an elegant way to handle some shared validations between two controllers.

Example:

I have two Accounts controllers. One to process accounts associations to a user synchronously (using, for instance, a PORO that contains the logic for this case), and another for treating the association asynchronously with a worker. Please assume that the logic differs in each scenario and the fact that being sync/async isn't the only difference.

Then I have this two controllers:

module Accounts
  class AssociationsController < ApplicationController
    def create
      return already_associated_account_error if user_has_some_account_associated?
      # action = call some account association PORO
      render json: action.response, status: action.status_code
    end

    private

    def user_has_some_account_associated?
      params[:accounts].any? { |account_number| user_account_associated?(account_number) }
    end

    def user_account_associated?(account_number)
      current_user.accounts.exists?(number: account_number)
    end

    def already_associated_account_error
      render json: 'You already have associated one or more accounts', status: :unprocessable_entity
    end
  end
end

Now I have another controller in which I'd want to apply the same validation:

module Accounts
  class AsyncAssociationsController < ApplicationController
    def create
      return already_associated_account_error if user_has_some_account_associated?
      # Perform asynchronously some account association WORKER
      render json: 'Your request is being processed', status: :ok
    end

    private

    def user_has_some_account_associated?
      params[:accounts].any? { |account_number| user_account_associated?(account_number) }
    end

    def user_account_associated?(account_number)
      current_user.accounts.exists?(number: account_number)
    end

    def already_associated_account_error
      render json: 'You already have associated one or more accounts', status: :unprocessable_entity
    end
  end
end

...

HOW and WHERE could I place the validation logic in ONLY ONE SPOT and use it in both controllers? I think in extracting to a concern at first, but I'm not sure if they are intended for this cases of validation logic only.

Upvotes: 1

Views: 376

Answers (2)

Toby 1 Kenobi
Toby 1 Kenobi

Reputation: 5037

For this you should use concerns. It's what's they are designed for.

Under the controllers directory make a concerns directory (if it isn't already there) and inside that make the file association_concern.rb with this content:

module AssociationConcern
  extend ActiveSupport::Concern

  private

  def user_has_some_account_associated?
    params[:accounts].any? { |account_number| user_account_associated?(account_number) }
  end

  def user_account_associated?(account_number)
    current_user.accounts.exists?(number: account_number)
  end

  def already_associated_account_error
    render json: 'You already have associated one or more accounts', status: :unprocessable_entity
  end

end

Anything that is common to the controllers can go in the concern

Then in your controllers simply include AssociationConcern

class AssociationsController < ApplicationController
  include AssociationConcern

  def create
    return already_associated_account_error if user_has_some_account_associated?
    # action = call some account association PORO
    render json: action.response, status: action.status_code
  end

end

Upvotes: 2

Marcin Kołodziej
Marcin Kołodziej

Reputation: 5313

Make them inherit from some new controller and add a before_action, like this:

module Accounts
  class AbstractAssociationsController < ApplicationController
    before_action :check_for_associated_account, only: [:create]

    def check_for_associated_account
      if user_has_associated_account?
        render json: 'You already have associated one or more accounts', status: :unprocessable_entity
      end
    end
  end
end

module Accounts
  class AssociationsController < AbstractAssociationsController
    def create
      # action = call some account association PORO
      render json: action.response, status: action.status_code
    end
  end
end

Then, depending on whether the logic is really different, you can define the user_has_associated_account? in either this abstract controller, separate controllers or delegate it to some PORO class.

Upvotes: 0

Related Questions