The Hanami web framework has brought a lot of best practices that are missing in Rails to the table. The organisation of a Hanami app is well designed to allow your app to go from a monolith to small or large services all running and sharing in one business codebase.

In Rails, your app is locked to your database, or simply ActiveRecord. Even your views have a clue what your database tables look like. And this can be very annoying.

Hanami has this little known thing called ‘the interactor’. If you are familiar with ‘services’, this is exactly that. Unfortunaletly its not a well documented feature in the docs, and as its not mentioned in the infamous ‘getting started’ section. It is easy to miss it.

This feature solves a lot of the problems that we burden controllers/actions with. In case you’re wondering where to find it, your can find it here. Lets look at how you can solve some problems.

Take an example of a long running process, say you want to upload and process pictures or talk to an external api. This kind of process can easily take longer than an HTTP request is allowed. Lets see how we can avoid trouble without monkey-jumping around.

Setup

Depending on how you load your interactor. The directory structure I use looks like this:

For all apps in the apps/ directory lib/<APPNAME>/interactors/upload/upload.rb. This way its required and included automatically project-wide.

For a specific app in the apps/ directory lib/interactors/upload/upload.rb. To load it in each app we need some more setup steps in each app:

  # apps/web/application.rb
  # ...
  load_paths << [
    'controllers',
    'views',
    '../../lib/interactors'
  ]
  # ...

The Interactor

Say we want to build an uploader that we can use all around. Lets get the base out of the way

# lib/interactors/upload/upload.rb
require 'hanami/interactor'
require 'hanami/utils/path_prefix'

module Uploader
  include Hanami::Interactor

  def initialize(tempfile, filename)
    @tempfile, @filename = tempfile, dest_dir.join(filename).to_s
    @uploaded_file_url = ''
  end

  def storage_location
    FileUtils.mkdir_p(root.join('public/uploads'))
  end

  # Root path
  def root
    ::Application.configuration.root
  end

  def upload
    begin
      FileUtils.cp(@tempfile.path, @filename.to_s)
    rescue => e
      e.backtrace.each { |msg| error(msg) }
      return
    ensure
      @tempfile.close
      @tempfile.unlink
    end

    @uploaded_file_url = Hanami::Utils::PathPrefix.new('uploads').join(@filename).to_s
  end
end

# lib/interactors/upload/image.rb
class ImageUploader
  include Uploader

  expose :image_url

  def initialize(params = {})
    @params = params
    @image_url = ''
  end

  def call
    copy_file
    process_file
    remove_temp_file
    image_url
  end

  private

  def copy_file

  end

  def process_file

  end

  # ...
end