Folding on cowboy_req
2018-02-05Streamlined use of a streamlined API
The problem
Pre-1.x versions of cowboy have an elegant, minimalist, and uniform API for
grabbing data out of http_req
records. This API includes functions like:
cowboy_req:header/3
, cowboy_req:qs_vals/1
, etc.
Their elegance and uniformity come from the fact that they all return the tuple
{A, NewReq}
, such that A
is the result of the function call, and NewReq
is the potentially-transformed, new request record that you should use
thereafter.
Suppose your business requirements dictate that you have to gather a lot of
data from a request, and then do_something()
with it. What ends up happening
is that your code begins to look like the following:
{OriginalIp, Req1} = cowboy_req:header(<<"x-forwarded-for">>, Req, <<>>),
{UserAgent, Req2} = cowboy_req:header(<<"user-agent">>, Req1, <<>>),
{QsVals, Req3} = cowboy_req:qs_vals(Req2),
{Path, Req4} = cowboy_req:path(Req4),
Reply = do_something(OriginalIp, UserAgent, QsVals, Path),
{ok, Reply, Req4}.
This gets tedious fast, and causes problems if you want to add or remove some
of the intermediate steps. For example: removing the assignment to Req2
would
force you to rename all following variables, or leave a 'hole' and rename
Req1
to Req2
in the previous line. Inelegant options, both.
The solution
As the wonderful tutorial on the universality and expresiveness of fold will
tell you, fold
is the best function. We can clean up the above code and
remove our useless assignments to numbered variables by using the following
pattern.
{[OriginalIp, UserAgent, QsVals, Path], NewReq} =
fold_req([fun(R) -> cowboy_req:header(<<"x-forwarded-for">>, R, <<>>) end,
fun(R) -> cowboy_req:header(<<"user-agent">>, R, <<>>) end,
fun(R) -> cowboy_req:qs_vals(R) end,
fun(R) -> cowboy_req:path(R) end]
Req),
Reply = do_something(OriginalIp, UserAgent, QsVals, Path),
{ok, Reply, NewReq}.
Yes, that's 2 extra lines of overhead, but the gain is that we don't have to
keep track of the various Req
s that abound in the code above.
All of the functions in the chain are completely independent, and we can add or remove them as we see fit, without having to rearrange all the code.
The implemenation
The implementation of fold_req/2
is trivial, so if you're up for the task,
try to write it yourself, now.
Scroll below to see my take on the solution.
----------------------------------------------------------
--------------------- SPOILERS BELOW ---------------------
----------------------------------------------------------
The actual implementation
fold_req(Funs, Req) ->
lists:foldr(fun fold_step/2, {[], Req}, Funs).
fold_step(Fun, {Acc, Req}) ->
{Val, NewReq} = Fun(Req),
{[Val|Acc], NewReq}.
Cheers!