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 = self.class.params_class.new(req.env)
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)
end
end
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[
"actions.saml_metadata.store_json",
"actions.saml_metadata.store_xml"
]
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)
end
end
end
end