File size: 3,191 Bytes
175aa40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
%%%-------------------------------------------------------------------
%%% @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])).