%%%------------------------------------------------------------------- %%% @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])).