Actions as objects have a lot of advantages but they make code sharing less intuitive. This section shares a few techniques to make this possible.

Prepare

In our settings (apps/web/application.rb), there is a code block that allows us to share the code for all the actions of our application. When an action includes the Web::Action module, that block code is yielded within the context of that class. This is heavily inspired by Ruby Module and its included hook.

Imagine we want to check if the current request comes from an authenticated user.

We craft a module in apps/web/controllers/authentication.rb.

  1. # apps/web/controllers/authentication.rb
  2. module Web
  3. module Authentication
  4. def self.included(action)
  5. action.class_eval do
  6. before :authenticate!
  7. expose :current_user
  8. end
  9. end
  10. private
  11. def authenticate!
  12. halt 401 unless authenticated?
  13. end
  14. def authenticated?
  15. !!current_user
  16. end
  17. def current_user
  18. @current_user ||= UserRepository.new.find(session[:user_id])
  19. end
  20. end
  21. end

Once included by an action, it will set a before callback that executes :authenticate! for each request. If not logged in, a 401 is returned, otherwise the flow can go ahead and hit #call. It also exposes current_user for all the views (see Exposures).

It will be really tedious to include this module for all the actions of our app. We can use controller.prepare for the scope.

  1. # apps/web/application.rb
  2. require_relative './controllers/authentication'
  3. module Web
  4. class Application < Hanami::Application
  5. configure do
  6. controller.prepare do
  7. include Web::Authentication
  8. end
  9. end
  10. end
  11. end

Code included via prepare is available for ALL the actions of an application.

Skipping A Callback

Let’s say we have included Authentication globally, but want to skip the execution of its callback for certain resources. A typical use case is to redirect unauthenticated requests to our sign in form.

The solution is really simple and elegant at the same time: override that method.

  1. # apps/web/controllers/sessions/new.rb
  2. module Web
  3. module Controllers
  4. module Sessions
  5. class New
  6. include Web::Action
  7. def call(params)
  8. # ...
  9. end
  10. private
  11. def authenticate!
  12. # no-op
  13. end
  14. end
  15. end
  16. end
  17. end

The action will still try to invoke :authenticate!, because, technically speaking, callbacks execution can’t be skipped. But if we override that method with an empty implementation, it does nothing and our non-signedin users can reach the wanted resource.

Module Inclusion

Imagine we have a RESTful resource named books. There are several actions (show, edit, update and destroy) which need to find a specific book to perform their job.

What if we want to DRY the code of all these actions? Ruby comes to our rescue.

  1. # apps/web/controllers/books/set_book.rb
  2. module Web
  3. module Controllers
  4. module Books
  5. module SetBook
  6. def self.included(action)
  7. action.class_eval do
  8. before :set_book
  9. end
  10. end
  11. private
  12. def set_book
  13. @book = BookRepository.new.find(params[:id])
  14. halt 404 if @book.nil?
  15. end
  16. end
  17. end
  18. end
  19. end

We have defined a module for our behavior to share. Let’s include it in all the actions that need it.

  1. # apps/web/controllers/books/update.rb
  2. require_relative './set_book'
  3. module Web
  4. module Controllers
  5. module Books
  6. class Update
  7. include Web::Action
  8. include SetBook
  9. def call(params)
  10. # ...
  11. end
  12. end
  13. end
  14. end
  15. end