3
# users_show_controller.rb
class Controllers::Users::Show
  include Hanami::Action

  params do
    required(:id).filled(:str?)
  end

  def call(params)
    result = users_show_interactor(id: params[:id])

    halt 404 if result.failure?
    @user = result.user
  end
end

# users_show_interactor.rb
class Users::Show::Interactor
  include Hanami::Interactor

  expose :user
  def call(:id)
    @user = UserRepository.find_by(:id)
  end
end

I have a controller and a interactor like above. And I'm considering the better way to distinguish ClientError from ServerError, on the controller.

I think It is nice if I could handle an error like below.

handle_exeption StandardError => :some_handler

But, hanami-interactor wraps errors raised inside themselves and so, controller receive errors through result object from interactor.

I don't think that re-raising an error on the controller is good way.

result = some_interactor.call(params)
raise result.error if result.failure

How about implementing the error handler like this? I know the if statement will increase easily and so this way is not smart.

def call(params)
  result = some_interactor.call(params)
  handle_error(result.error) if result.faulure?
end

private

def handle_error(error)
  return handle_client_error(error) if error.is_a?(ClientError)
  return server_error(error) if error.is_a?(ServerError)
end

3 Answers3

3

Not actually hanami-oriented way, but please have a look at dry-monads with do notation. The basic idea is that you can write the interactor-like processing code in the following way

def some_action
  value_1 = yield step_1
  value_2 = yield step_2(value_1)
  return yield(step_3(value_2))
end 

def step_1
  if condition
    Success(some_value)
  else
    Failure(:some_error_code)
  end
end

def step_2
  if condition
    Success(some_value)
  else
    Failure(:some_error_code_2)
  end
end

Then in the controller you can match the failures using dry-matcher:

matcher.(result) do |m|
  m.success do |v|
    # ok
  end

  m.failure :some_error_code do |v|
    halt 400
  end

  m.failure :some_error_2 do |v|
    halt 422
  end
end

The matcher may be defined in the prepend code for all controllers, so it's easy to remove the code duplication.

Aleksander Pohl
  • 1,675
  • 10
  • 14
0

Hanami way is validating input parameters before each request handler. So, ClientError must be identified always before actions logic.

halt 400 unless params.valid? #halt ClientError
#your code
result = users_show_interactor(id: params[:id])
halt 422 if result.failure? #ServerError
halt 404 unless result.user
@user = result.user
Leo
  • 1,673
  • 1
  • 13
  • 15
  • Thanks. It seems right. But the parameter validation can handle only BadRequest. How can I return different status code by the error raised inside of interactor? When `result.failure?` is true, I must know it failed with Database Error or RecordNotFound to return correct status code. I want to know the nice way to hide such a complexity from code. – user8346437 Jul 31 '18 at 09:45
  • Raise an error on the wrapper is not a good idea. `nil` better than force exit with error RecordNotFound. Of course, you can do single `halt handle_error`, but no reason do something that would fail. – Leo Jul 31 '18 at 10:15
0

I normally go about by raising scoped errors in the interactor, then the controller only has to rescue the errors raised by the interactor and return the appropriate status response.

Interactor:

module Users
  class Delete
    include Tnt::Interactor

    class UserNotFoundError < ApplicationError; end

    def call(report_id)
      deleted = UserRepository.new.delete(report_id)

      fail_with!(UserNotFoundError) unless deleted
    end
  end
end

Controller:

module Api::Controllers::Users
  class Destroy
    include Api::Action
    include Api::Halt

    params do
      required(:id).filled(:str?, :uuid?)
    end

    def call(params)
      halt 422 unless params.valid?

      Users::Delete.new.call(params[:id])
    rescue Users::Delete::UserNotFoundError => e
      halt_with_status_and_error(404, e)
    end
  end
end

fail_with! and halt_with_status_and_error are helper methods common to my interactors and controllers, respectively.

# module Api::Halt
def halt_with_status_and_error(status, error = ApplicationError)
  halt status, JSON.generate(
    errors: [{ key: error.key, message: error.message }],
  )
end

# module Tnt::Interactor
def fail_with!(exception)
  @__result.fail!
  raise exception
end
Rodrigo Vasconcelos
  • 1,270
  • 2
  • 14
  • 26