Different views for one action in Hanami

Hanami used to be called Lotus before. That remains in the URL, but I refer to the framework as Hanami in the post now.

Use case

I’m recently writing a MediaWiki-like app in Hanami as a training. We all know Wikipedia-like URLs, eg. https://en.wikipedia.org/wiki/Ruby. Essentially we can break it down into routing:

get /wiki/:id, to: 'pages#show'

What happens, though, if there is no page with id “Ruby”? MediaWiki will tell you that a given page does not exist and suggests you what further actions can be taken: write one, search, check linking pages, to name a few. Moreover, if the page was there but was since then deleted (due to copyright infringement or simply poor quality), you will see an information about that, so you don’t feel completely lost.

I wanted to replicate that behaviour within my Hanami application. It turned out to be not so easy. Of course, I could get away with one huge template like:

<% if @page.exists? %>
...
<% elsif @page.deleted? %>
...
<% else %>
...
<% end %>

But after something like that I could never show my face around any serious Rubyists of the world. Besides it is not exactly about template. I might want have different fragments of logic there and, as far as I understand Hanami philosophy, it is another view I want to plug into my action.

A proper way

Fortunately achieving it is not so hard. I would say it is even pretty straightforward, although may seem very explicit for people that came from Rails-world (me being one of them). Basically, you need this:

def call(params)
  @page = PageRepository.by_title(params[:id])
  if @page.nil?
    @page = Page.new(title: params[:id])
    self.body = Web::Views::Pages::NotFound.render(exposures)
  end
end

What I did here is using a low-level Hanami::Controller API by explicitly setting a body of a response. Hanami will add all the headers headers etc. and will return it to the client’s browser. What’s in that body? That comes from Hanami::View API and is as simple as rendering a view (which is a standalone unit, not tightly coupled with anything else from the stack). I passed current action’s exposures to it - which in my case is just a :page object).

If @page is not nil, I let Hanami to handle view in its standard way – find a view with matching name and also render it with current action’s exposures. I’ll explain it in more detail later.

What’s important is that now I have a behaviour I wanted and could easily stopped right here. Unless, of course, I want to dig deeper…

What really happens

Originally I intended to name this section A magic way but Hanami turned out to be quite immune to magic. Because of that I can only explain how the rendering and choosing view for action works.

When it comes do decide which view to render, Hanami uses a RenderingPolicy class for that. You can find its code at the time of writing this post here. Two methods are of particular interest for us right now (note that both are private!):

def _render_action(action, response)
  if successful?(response)
    view_for(action, response).render(
      action.exposures
    )
  end
end

def view_for(action, response)
  if response[BODY].empty?
    captures = @controller_pattern.match(action.class.name)
    Utils::Class.load!(@view_pattern % { controller: captures[:controller], action: captures[:action] }, @namespace)
  else
    Views::NullView.new(response[BODY])
  end
end

The first one checks if the response is successful (i.e. if its status is 200 or 201). If so, the view_for method is called with instance of our action and the response as arguments. This method checks if body is present in the response already. When I did self.body = Web::Views::Pages::NotFound.render(exposures) above, this is exactly what happened. So the policy now renders this response within a special NullView, which does nothing more than rendering the body with no changes.

If the body of the response is empty (the second path in my code above), Hanami loads a view according to the @view_pattern, which is 'Views::%{controller}::%{action}' by default.

Either way, a view is rendered with current action’s exposures, just like we did in our controller above.

My idea of magical solution was to use some special exposed variable (my thought was view_class) and extend view_for to use it, if present. Of course, just opening the class and overriding its private methods is completely wrong, so it should be done in a separate class that inherits from RenderingPolicy. It failed because I haven’t found any way to inject my smart policy into Hanami application. Maybe it will be possible in future. Or maybe not.

Epilogue

Since the very idea of trying to perform some magic may rise concerns, I feel obliged to explain how I feel about that. It is generally wrong to abuse it. It is definitely wrong when you use it extensively in shared code, common codebases etc. It’s just bad to give it away to anyone out there.

However, if you know what you are doing, it is perfectly fine to use it for your own benefit. You will learn, probably a lot, about the system that you want to magicize. You can also accidentally burn some village or even end up inventing another ActiveSupport, but still it will probably worth a try.

Anyway, Hanami’ system of view handling turned out to be powerful enough to easily handle my requirements, so there is not really a need to try to bend it. Other than educational.

Related posts