Subordinate Actions, or: Dynamic Dispatch

Consider this a debrief, basically, of one of the addendums/alterations I have made to the framework as part of my work on what is likely one of the top most complex Hanami 2 apps that exists currently. (I have no way to prove this, but I think it’s very likely).

Hanami’s approach of treating Actions as single functions that we can map to individual routes is an excellent antidote to Rails’ controller maximalism, and in most cases works perfectly.

However, I have run into a situation where I needed something a little more elaborate and maybe this can serve as a useful data point.

I have an endpoint that accepts SAML metadata. SAML metadata for my purposes is three values: an ID, a URI, and a Certificate.

There are multiple ways someone might want to set these values. Some people prefer to set them individually, and certificates need to be updated separately in any case. But a lot of people would prefer to upload a standardized XML file that contains this information. So I need to accept two different media types.

Accepting two media types creates a problem with params. While this is quite useful for JSON, it’s an inappropriate construct for XML data. So I if I define params_class, it needs to only be used for JSON requests.

At first, I tried to implement this as a single Action. That quickly got out of hand; the differing validation rules made this complex.

So, I changed my approach and built a way for an Action to dispatch dynamically to another Action.

class Action < Hanami::Action
  def subordinate(req, res)
	params =
	sub_req = build_request(env: req.env, params:)
	_run_before_callbacks(sub_req, res)
	handle(sub_req, res)
	_run_after_callbacks(sub_req, res)
  rescue => err
    _handle_exception(sub_req, res, err)

This is basically an abbreviated form of what Hanami::Action#call does, but it leaves open the possibility to do some of the same work in the parent action before returning the response.

Then, I include the subordinate actions as dependencies of the dispatch action and call the appropriate one.

module Actions::SAMLMetadata
  class Store < Action
    include Deps[
    format :json, :metadata
    def handle(req, res)
	  case config.formats.format_for(req.content_type)
	  when :json
	    store_json.subordinate(req, res)
	  when :metadata
	    store_xml.subordinate(req, res)
1 Like

Hanami offers JSON parsing OOTB because the parsing is universal.
We couldn’t do the same with XML.

My suggestion is to build a custom parser for your needs (see the docs). Just inherit from Hanami::Middleware::BodyParser and mount it in the Rack middleware of the app.

The result of the parsing would be a Hash that can be passed to the Params class.

That would work, we had a preexisting parser from back when this was a dry-system app that Uses Nokogiri + XPath to produce a struct object.