hyperkitty-chromium / src /hk_api_util.erl
SNAPKITTYWEST's picture
Add src/hk_api_util.erl
175aa40 verified
Raw
History Blame Contribute Delete
3.19 kB
%%%-------------------------------------------------------------------
%%% @doc Shared helpers for the Cowboy request handlers under
%%% `hk_api_sup': request-id propagation, JSON body/response
%%% handling, and the accepted/succeeded/failed operation-tracking
%%% wrapper every mutating endpoint uses.
%%% @end
%%%-------------------------------------------------------------------
-module(hk_api_util).
-include("hyperkitty.hrl").
-export([request_id/1, read_json_body/1, reply_json/3, reply_error/3,
run_operation/4]).
%% @doc Every request has a request id: reuse the caller-supplied
%% `x-request-id' header if present (so a client's own tracing
%% correlates), otherwise mint one.
-spec request_id(cowboy_req:req()) -> binary().
request_id(Req) ->
case cowboy_req:header(<<"x-request-id">>, Req) of
undefined -> hk_id:new(<<"req">>);
Id -> Id
end.
-spec read_json_body(cowboy_req:req()) -> {ok, map(), cowboy_req:req()} | {error, term()}.
read_json_body(Req) ->
case cowboy_req:has_body(Req) of
false ->
{ok, #{}, Req};
true ->
{ok, Body, Req2} = cowboy_req:read_body(Req),
try {ok, jsx:decode(Body, [return_maps]), Req2}
catch _:_ -> {error, invalid_json}
end
end.
-spec reply_json(non_neg_integer(), term(), cowboy_req:req()) -> cowboy_req:req().
reply_json(Status, Data, Req) ->
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>},
jsx:encode(Data), Req).
-spec reply_error(non_neg_integer(), term(), cowboy_req:req()) -> cowboy_req:req().
reply_error(Status, Reason, Req) ->
reply_json(Status, #{error => term_bin(Reason)}, Req).
%% @doc Run a mutating operation with full lifecycle tracking:
%% create an `accepted' `#operation{}', invoke `Fun/0', mark the
%% operation `succeeded'/`failed' based on the result, and reply
%% with `{operation_id, ..., result: ...}' or an error body. `Fun/0'
%% must return `{ok, Subject, ResultMap}' or `{error, Reason}'.
-spec run_operation(binary(), atom(), fun(() -> {ok, term(), map()} | {error, term()}),
cowboy_req:req()) -> cowboy_req:req().
run_operation(Kind, RequestId, Fun, Req) ->
hk_operation_store:ensure_table(),
Op = hk_operation_store:create(RequestId, Kind, undefined),
case Fun() of
{ok, Subject, ResultMap} ->
hk_operation_store:complete(Op#operation.operation_id, Subject),
reply_json(200, #{operation_id => Op#operation.operation_id,
request_id => RequestId,
status => succeeded,
result => ResultMap}, Req);
{error, Reason} ->
hk_operation_store:fail(Op#operation.operation_id, #{reason => term_bin(Reason)}),
reply_json(422, #{operation_id => Op#operation.operation_id,
request_id => RequestId,
status => failed,
error => term_bin(Reason)}, Req)
end.
term_bin(Term) when is_binary(Term) -> Term;
term_bin(Term) -> iolist_to_binary(io_lib:format("~p", [Term])).