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