The list of challenges in my companion app finally shows each challenge’s difficulty and points. Five stars for the difficulty, and the word “point” agreeing with the number. Two small display methods, and a question I answer on autopilot in Rails: where do they go?
This series keeps twenty years of Rails reflexes switched on inside a Hanami 3 application, a small CTF challenge tracker, and records where they stop matching what the framework does. Most posts so far have been about Rails against Hanami. This one is about Hanami 2 against Hanami 3, and about what happens when the version you learned from is not the version you run.
A helper first, as in Rails
The first draft went where it would go in Rails, into a helper module:
def difficulty_stars(difficulty)
("★" * difficulty) + ("☆" * (5 - difficulty))
end
def points_label(points)
points == 1 ? "1 point" : "#{points} points"
end
Hanami generates app/views/helpers.rb with a comment inviting you to add
helpers there, and nothing needs registering. The template called
difficulty_stars(challenge.difficulty) and the request spec went green on
the first run. No friction, and no pluralize helper either, hence the
ternary.
Then the presenter I would write
Two helpers that each take one attribute of the same object are where I stop writing helpers in Rails. I do not use a gem for this, except in the rare case where one brings something real. I usually write a small base class, then one presenter per thing I present. Stripped down, on this app’s example, it looks like this:
class BasePresenter
def initialize(object, view)
@object = object
@view = view
end
def self.presents(name)
define_method(name) { @object }
end
def method_missing(...)
@view.send(...)
end
def respond_to_missing?(...)
@view.respond_to?(...)
end
end
class ChallengePresenter < BasePresenter
presents :challenge
def difficulty_stars
tag.span(("★" * challenge.difficulty) + ("☆" * (5 - challenge.difficulty)), class: "stars")
end
end
The template builds it explicitly, at the moment it needs it:
<% @challenges.each do |challenge| %>
<% presenter = ChallengePresenter.new(challenge, self) %>
<li><%= challenge.name %> <%= presenter.difficulty_stars %></li>
<% end %>
Rendered with Action View 8.1.4.1 in a small script, that gives what you would expect, and it also shows where an unknown method goes:
<li>Stack Smash <span class="stars">★★★★☆</span></li>
presenter.name -> NoMethodError: undefined method 'name' for an instance of #<Class:0x00000001242c19b8>
tag works inside the presenter because anything the presenter does not
know is sent to the view. The record is reached through the name given to
presents, never implicitly, which is why presenter.name lands on the view
and fails there.
The part that never turned up
Hanami’s name for that object is a part, and I had read enough of the 2.x
guides to expect the trade: name the class after the exposure, and the view
wraps the value for you. So I moved the two methods into
app/views/parts/challenge.rb:
# auto_register: false
# frozen_string_literal: true
module CtfTracker
module Views
module Parts
class Challenge < CtfTracker::Views::Part
def difficulty_stars
("★" * difficulty) + ("☆" * (5 - difficulty))
end
def points_label
points == 1 ? "1 point" : "#{points} points"
end
end
end
end
end
The template now says challenge.difficulty_stars. A unit spec built the part
by hand with described_class.new(value: challenge) and passed. The request
spec did not:
undefined method 'difficulty_stars' for an instance of CtfTracker::Structs::Challenge (attribute not loaded?)
That parenthesis comes from ROM, not from Hanami, and it sent me the wrong way for a minute. It suggests a column missing from the query, when the real news is in the class name before it: the template was holding the bare struct. No part had been built at all.
Checking every link before touching one
The obvious suspect was the lookup: a namespace the view does not search, or a class it cannot find. Rather than change configuration on a guess, I asked the view, in a script, for each link of the chain:
part namespace: CtfTracker::Views::Parts
class it finds: CtfTracker::Views::Parts::Challenge
is a part: true
decorated by default: false
challenges decorated: false
The first three lines were my suspects, and all three are fine. The last two explain everything. The view knows where parts live, finds mine, accepts it as a part, and then never asks for it, because nothing told it to. The reason is in the hanami-view 3.0.0 changelog, under Changed:
- **BREAKING:** Default to undecorated exposures. (@timriley in #274)
Decorate your exposures with the new `.decorate` method, or the `decorate: true` option for `.expose`. Default back to decorated exposures via `config.decorate_exposures = true`.
Reconfiguring the namespace, the obvious fix, would have changed nothing.
One word in the view
The fix is one word, in the view class rather than the template:
class Index < CtfTracker::View
decorate :challenges
expose :sorts
expose :filters
end
Run the same script against that view and the last line flips to
challenges decorated: true. Everything else I knew about parts was still
true: the collection exposure is singularised to find Parts::Challenge, and
each item is wrapped. One default had moved under it.

The list at 08-ask-for-the-part, with made-up data. The stars, the points
and the solver all come from the part. The app has no styling at all: it is a
sandbox for the framework, not a product.
The part also forwards what it does not know to the value it wraps, which is
why difficulty and points work inside it without a prefix:
part.name -> "Stack Smash"
part.difficulty_stars -> "★★★★☆"
part.helpers -> CtfTracker::Views::Part::PartHelpers
Helpers are still there, through an explicit helpers object rather than
through method_missing. Put next to my Rails presenter, the delegation runs
the other way round: my presenter sends the unknown to the view and names the
record, the part sends the unknown to the record and names the helpers.
Closer to my Rails habit than I expected
The irony is that Hanami 3 is closer to how I already work in Rails than the version I learned it from. My presenters do not appear by themselves: a template asks for one, by name, when it needs one. Hanami 3 still lets you switch the old default back on, so I did, on the same view, to see what it used to cost:
as committed (decorate :challenges)
challenges CtfTracker::Views::Part
sorts Array
filters Hash
each challenge: CtfTracker::Views::Parts::Challenge
with decorate_exposures = true
challenges CtfTracker::Views::Part
sorts CtfTracker::Views::Part
filters CtfTracker::Views::Part
each challenge: CtfTracker::Views::Parts::Challenge
With decoration on by default, an exposure that has no part of its own is wrapped in the base part class anyway, so the list of sort columns and the hash of current filters reach the template inside objects with nothing to add. Now only what the view asks for is wrapped, and the view says so in one place you can read.
I think the change is reasonable. The cost of decorating is now paid where decoration is wanted, and a view class tells you which of its exposures carry behaviour. What it does make harder is noticing a part you forgot to ask for: an undecorated exposure is the default, not an error, so the template keeps working until it calls a method that only the part has. The only symptom I got was a message from another library pointing at the database.
I tripped because my knowledge was one major version old, which is the
ordinary condition of anyone who learns a framework from its guides and
then upgrades it. The part, its spec and the one-word fix are in the
companion app at
08-ask-for-the-part.
Comments, questions, or just a reaction?
Send an email to ~bounga/bounga.org-discuss@lists.sr.ht. It is public and archived, so other readers can follow along and answer too.