Add src/hk_api_util.erl
Browse files- src/hk_api_util.erl +72 -0
src/hk_api_util.erl
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
%%%-------------------------------------------------------------------
|
| 2 |
+
%%% @doc Shared helpers for the Cowboy request handlers under
|
| 3 |
+
%%% `hk_api_sup': request-id propagation, JSON body/response
|
| 4 |
+
%%% handling, and the accepted/succeeded/failed operation-tracking
|
| 5 |
+
%%% wrapper every mutating endpoint uses.
|
| 6 |
+
%%% @end
|
| 7 |
+
%%%-------------------------------------------------------------------
|
| 8 |
+
-module(hk_api_util).
|
| 9 |
+
|
| 10 |
+
-include("hyperkitty.hrl").
|
| 11 |
+
|
| 12 |
+
-export([request_id/1, read_json_body/1, reply_json/3, reply_error/3,
|
| 13 |
+
run_operation/4]).
|
| 14 |
+
|
| 15 |
+
%% @doc Every request has a request id: reuse the caller-supplied
|
| 16 |
+
%% `x-request-id' header if present (so a client's own tracing
|
| 17 |
+
%% correlates), otherwise mint one.
|
| 18 |
+
-spec request_id(cowboy_req:req()) -> binary().
|
| 19 |
+
request_id(Req) ->
|
| 20 |
+
case cowboy_req:header(<<"x-request-id">>, Req) of
|
| 21 |
+
undefined -> hk_id:new(<<"req">>);
|
| 22 |
+
Id -> Id
|
| 23 |
+
end.
|
| 24 |
+
|
| 25 |
+
-spec read_json_body(cowboy_req:req()) -> {ok, map(), cowboy_req:req()} | {error, term()}.
|
| 26 |
+
read_json_body(Req) ->
|
| 27 |
+
case cowboy_req:has_body(Req) of
|
| 28 |
+
false ->
|
| 29 |
+
{ok, #{}, Req};
|
| 30 |
+
true ->
|
| 31 |
+
{ok, Body, Req2} = cowboy_req:read_body(Req),
|
| 32 |
+
try {ok, jsx:decode(Body, [return_maps]), Req2}
|
| 33 |
+
catch _:_ -> {error, invalid_json}
|
| 34 |
+
end
|
| 35 |
+
end.
|
| 36 |
+
|
| 37 |
+
-spec reply_json(non_neg_integer(), term(), cowboy_req:req()) -> cowboy_req:req().
|
| 38 |
+
reply_json(Status, Data, Req) ->
|
| 39 |
+
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>},
|
| 40 |
+
jsx:encode(Data), Req).
|
| 41 |
+
|
| 42 |
+
-spec reply_error(non_neg_integer(), term(), cowboy_req:req()) -> cowboy_req:req().
|
| 43 |
+
reply_error(Status, Reason, Req) ->
|
| 44 |
+
reply_json(Status, #{error => term_bin(Reason)}, Req).
|
| 45 |
+
|
| 46 |
+
%% @doc Run a mutating operation with full lifecycle tracking:
|
| 47 |
+
%% create an `accepted' `#operation{}', invoke `Fun/0', mark the
|
| 48 |
+
%% operation `succeeded'/`failed' based on the result, and reply
|
| 49 |
+
%% with `{operation_id, ..., result: ...}' or an error body. `Fun/0'
|
| 50 |
+
%% must return `{ok, Subject, ResultMap}' or `{error, Reason}'.
|
| 51 |
+
-spec run_operation(binary(), atom(), fun(() -> {ok, term(), map()} | {error, term()}),
|
| 52 |
+
cowboy_req:req()) -> cowboy_req:req().
|
| 53 |
+
run_operation(Kind, RequestId, Fun, Req) ->
|
| 54 |
+
hk_operation_store:ensure_table(),
|
| 55 |
+
Op = hk_operation_store:create(RequestId, Kind, undefined),
|
| 56 |
+
case Fun() of
|
| 57 |
+
{ok, Subject, ResultMap} ->
|
| 58 |
+
hk_operation_store:complete(Op#operation.operation_id, Subject),
|
| 59 |
+
reply_json(200, #{operation_id => Op#operation.operation_id,
|
| 60 |
+
request_id => RequestId,
|
| 61 |
+
status => succeeded,
|
| 62 |
+
result => ResultMap}, Req);
|
| 63 |
+
{error, Reason} ->
|
| 64 |
+
hk_operation_store:fail(Op#operation.operation_id, #{reason => term_bin(Reason)}),
|
| 65 |
+
reply_json(422, #{operation_id => Op#operation.operation_id,
|
| 66 |
+
request_id => RequestId,
|
| 67 |
+
status => failed,
|
| 68 |
+
error => term_bin(Reason)}, Req)
|
| 69 |
+
end.
|
| 70 |
+
|
| 71 |
+
term_bin(Term) when is_binary(Term) -> Term;
|
| 72 |
+
term_bin(Term) -> iolist_to_binary(io_lib:format("~p", [Term])).
|