July was a great month for my work on Hanami!
After a feeling like I stalled a little in June, this time around I was able to get to the very end of my initial plans for application/action/view integration, as well as improve clarity around comes next for our 2.0 efforts overall.
Whenever I’ve worked on integrating the configuration of the standalone Hanami components (like hanami-controller or view) into the application core, I’ve asked myself, “if the app author chose not to use this component, would the application-level configuration still make sense?” I wanted to avoid baking in too many assumptions about hanami-controller or hanami-view particulars into the config that you can access on Hanami.application.config
.
In the long term, I hope we can build a clean extensions API so that component gems can cleanly register themselves with the framework and expose their configuration that way. In the meantime, however, we need to take a practical, balanced approach, to make it easy for hanami-controller and hanami-view to do their job while still honouring that longer-term goal in spirit.
I’m happy to report that I think I’ve found a pretty good arrangement for all of this! You can see it in action with we we load the application.config.actions
configuration:
module Hanami
class Configuration
attr_reader :actions
def initialize(env:)
# ...
@actions = begin
require_path = "hanami/action/application_configuration"
require require_path
Hanami::Action::ApplicationConfiguration.new
rescue LoadError => e
raise e unless e.path == require_path
Object.new
end
end
end
end
With this approach, if the hanami-controller gem is available, then we’ll make its own ApplicationConfiguration
available as application.config.actions
. This means the hanami gem itself doesn’t need to know anything else about how action configuration should be handled at the application level. This kind of detail makes much more sense to live in the hanami-controller gem, where those settings will actually be used.
Let’s take a look at that:
module Hanami
class Action
class ApplicationConfiguration
include Dry::Configurable
# Define settings that are _specific_ to application integration
setting :name_inference_base, "actions"
setting :view_context_identifier, "view.context"
setting :view_name_inferrer, ViewNameInferrer
setting :view_name_inference_base, "views"
# Then clone all the standard settings from Action::Configuration
Configuration._settings.each do |action_setting|
_settings << action_setting.dup
end
def initialize(*)
super
# Apply defaults to standard settings for use within an app
config.default_request_format = :html
config.default_response_format = :html
end
# ...
end
end
end
This configuration class:
Hanami::Action
behaviour activated only when used within a full Hanami appHanami::Action::Configuration
(which are there for standalone use) and makes them availableThis feels like an ideal arrangement. It keeps the ApplicationConfiguration
close to the code in ApplicationAction
, which uses those new settings. It means that all the application integration code can live together and evolve in sync.
Further, because Hanami::Action::ApplicationConfiguration
exposes a superset of the base Hanami::Action::Configuration
settings, we can make it so any ApplicationAction
(i.e. any action defined within an Hanami app) automatically configures every aspect of itself based on whatever settings are available on the application!
So for the application author, the result of all this groundwork should be a blessedly unsurprising experience: if they’re using hanami-controller, then they can go and tweak whatever settings they want right there on Hanami.application.config.actions
, both the basic action settings as well as the integration-specific settings (though most of the time, I hope the defaults should be fine!).
When we do eventually implement an extensions API, we can at that point just remove the small piece special-case code from Hanami::Application::Configuration
and replace it with hanami-controller reigstering itself and making its settings available.
If you’re interested in following these changes in more detail, check out hanami/hanami#1068 for the change from the framework side, and then hanami/controller#321 for the ApplicationConfiguration
and hanami/controller#320 for the self-configuring application actions. (I also took an initial pass at this in hanami/hanami#1065, but that was surpassed by all the changes linked previously - I took small steps, and learnt along the way!)
I also made matching changes to view configuration. All the same ideas apply: if you have hanami-view loaded, you’ll find an Hanami.application.config.views
with all the view settings you need, and then application views will self-configure themselves based on those values! Check out hanami/hanami#1066 and hanami/view#176 for the implementation.
handle_exception
inside actionsOne of the settings on Hanami::Action classes is its array of config.handled_exceptions
, which you can also supply one-by-one through the config.handle_exception
convenience method.
It turns out another handle_exception
still existed as a class method, clearly an overhang of the previous action behaviour. I took care of removing that, so now there should be no confusion whenever action authors configure this behaviour (especially since the old class-level method didn’t work with inheritence).
Believe it or not, the work so far only took me about half-way through the month! This left enough time to roll through all my remaining “minimum viable action/view integration” tasks!
First up was inferring paired views for actions. The idea here is that if you’re building an Hanami 2 app and following the sensible convention of matching your view and action names, then the framework can take care of auto-injecting an action’s view for you.
So if you had an action class like this:
class Main
module Actions
module Articles
class Index < Main::Action
include Deps[view: "views.articles.index"]
def handle(request, response)
response.render view
end
end
end
end
end
Now, you can drop that include Deps[view: "…"]
line. A matching view will now automatically be available as the view
for the action!
This works even for RESTful-style actions too. For example, an Actions::Articles::Create
action would have an instance of Views::Articles::New
injected, since that’s the view you’d want to re-render in the case of presenting a form with errors.
If you need it, you can also configure your own custom view inference by providing your own Hanami.application.config.actions.view_name_inferrer
object.
To learn more about the implementation, check out the PR and then this follow-up fix (in which I learnt I should always write integration tests that exercise at least two levels of inheritance).
With the paired view inference above, our action class is now looking like this:
class Main
module Actions
module Articles
class Index < Main::Action
def handle(request, response)
response.render view
end
end
end
end
end
But we can do better. For simple actions like this, we shouldn’t have to write that “please render your own view” boilerplate.
So how about just this?
class Main
module Actions
module Articles
class Index < Main::Action
end
end
end
end
Now, any call to this action will automatically render its paired view, passing through all request params for the view to handle as required.
And the beauty of this change was that, after all the groundwork laid so far, it was only a single line of code!
As Kent Beck has said, “for each desired change, make the change easy (warning: this may be hard), then make the easy change.” The easy change indeed. Moments like these are why I love being a programmer :)
Let’s keep going! This month I also gave the “automatic application integration” treatment to Hanami::View::Context
. Now when you inherit from this within your application, it’ll be all set up to accept the request/response details that Hanami::Action
is already passing through whenever you render a view from within an action.
With these in place, we’re now providing useful methods like session
and flash
for use within your view-related classes and templates. If you want to add additional behaviour, you can now access request
and response
on your application’s view context class, too.
While I was doing this, I also took the opportunity to hash out some initial steps towards a standard library of view context helpers with an Hanami::View::ContextHelpers::ContentHelpers
module. If you mix this into your app’s view context class, you’ll also have a convenient content_for
method that works like you’d expect. Longer term, I’ll look to move this into the hanami-helpers gem and update the existing helpers to work with the new views, including providing a nice way to opt in to whatever specific helpers you want your application to expose.
In the meantime, check out all this fresh view context goodness here.
After all of this, I took a moment to update my Hanami 2 application template. If you create an app from this template today, all the features I’ve described above will be in place and ready for you to try. I also enabled rack session middleware in the app, because this is a requirement for the flash and session objects as well as CSRF protection.
Last but not least, as I was finally seeing some clear air ahead, I took a chance to bring our Hanami 2.0 Trello board up to date!
As it currently stands, I have just 7-8 items left before I think we’ll be ready for the long-awaited Hanami 2.0.0.alpha2 release.
Beyond that, I hope the board will help everyone coordinate the remainder of our work in preparing 2.0.0. At very least, I’m already feeling much better knowing we’re a little more oranized, with a single, up-to-date place where it’s easy to see what’s next as well as add new items whenever we think of them (I’ve no doubt that plenty more little things will crop up).
So that was July. What. A. Month.
I tell you, I was exceedingly happy to have finally completed my “get views and actions properly working together for the first time” list, which turns out to have taken the better past of five months.
For August, I plan to knock out as many of my remaining 2.0.0.alpha2 tasks. Some of them are pretty minor, but one or two are looming a little larger. We’ll see how many I can get through. One thing I’m accepting more and more is that when open sourcing across nights and weekends, patience is a virtue.
Thanks for sticking with me through this journey so far!
I’ve been working really hard on preparing a truly powerful, flexible Ruby application framework. I’m in this for the long haul, but it’s not easy.
If you’d like to help all of this come to fruition, I’d love for you to sponsor my open source work.
Thanks especially to Benjamin Klotz for your continued support.