SNAPKITTYWEST commited on
Commit
175aa40
·
verified ·
1 Parent(s): 1540772

Add src/hk_api_util.erl

Browse files
Files changed (1) hide show
  1. 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])).