./ct_report/coverage/mongoose_api.COVER.html

1 %%==============================================================================
2 %% Copyright 2014 Erlang Solutions Ltd.
3 %%
4 %% Licensed under the Apache License, Version 2.0 (the "License");
5 %% you may not use this file except in compliance with the License.
6 %% You may obtain a copy of the License at
7 %%
8 %% http://www.apache.org/licenses/LICENSE-2.0
9 %%
10 %% Unless required by applicable law or agreed to in writing, software
11 %% distributed under the License is distributed on an "AS IS" BASIS,
12 %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 %% See the License for the specific language governing permissions and
14 %% limitations under the License.
15 %%==============================================================================
16 -module(mongoose_api).
17
18 -behaviour(mongoose_http_handler).
19 -behaviour(cowboy_rest).
20
21 %% mongoose_http_handler callbacks
22 -export([config_spec/0, routes/1]).
23
24 %% cowboy_rest callbacks
25 -export([init/2,
26 terminate/3,
27 allowed_methods/2,
28 content_types_provided/2,
29 content_types_accepted/2,
30 delete_resource/2]).
31
32 -export([to_xml/2,
33 to_json/2,
34 from_json/2]).
35
36 -ignore_xref([behaviour_info/1, cowboy_router_paths/2, from_json/2, to_json/2, to_xml/2]).
37
38 -record(state, {handler, opts, bindings}).
39
40 -type prefix() :: string().
41 -type route() :: {string(), options()}.
42 -type routes() :: [route()].
43 -type bindings() :: proplists:proplist().
44 -type options() :: [any()].
45 -type method() :: get | post | put | patch | delete.
46 -type methods() :: [method()].
47 -type response() :: ok | {ok, any()} | {error, atom()}.
48 -export_type([prefix/0, routes/0, route/0, bindings/0, options/0, response/0, methods/0]).
49
50 -callback prefix() -> prefix().
51 -callback routes() -> routes().
52 -callback handle_options(bindings(), options()) -> methods().
53 -callback handle_get(bindings(), options()) -> response().
54 -callback handle_post(term(), bindings(), options()) -> response().
55 -callback handle_put(term(), bindings(), options()) -> response().
56 -callback handle_delete(bindings(), options()) -> response().
57
58 -include("mongoose_config_spec.hrl").
59
60 -type handler_options() :: #{path := string(), handlers := [module()], atom() => any()}.
61
62 %%--------------------------------------------------------------------
63 %% mongoose_http_handler callbacks
64 %%--------------------------------------------------------------------
65 -spec config_spec() -> mongoose_config_spec:config_section().
66 config_spec() ->
67 83 HandlerModules = [mongoose_api_metrics, mongoose_api_users],
68 83 #section{items = #{<<"handlers">> => #list{items = #option{type = atom,
69 validate = {enum, HandlerModules}},
70 validate = unique_non_empty}
71 },
72 defaults = #{<<"handlers">> => HandlerModules}}.
73
74 -spec routes(handler_options()) -> mongoose_http_handler:routes().
75 routes(#{path := BasePath, handlers := HandlerModules}) ->
76 155 lists:flatmap(fun(Module) -> cowboy_routes(BasePath, Module) end, HandlerModules).
77
78 cowboy_routes(BasePath, Module) ->
79 310 [{[BasePath, Module:prefix(), Path], ?MODULE, #{module => Module, opts => Opts}}
80 310 || {Path, Opts} <- Module:routes()].
81
82 %%--------------------------------------------------------------------
83 %% cowboy_rest callbacks
84 %%--------------------------------------------------------------------
85 init(Req, #{module := Module, opts := Opts}) ->
86 626 Bindings = maps:to_list(cowboy_req:bindings(Req)),
87 626 State = #state{handler = Module, opts = Opts, bindings = Bindings},
88 626 {cowboy_rest, Req, State}. % upgrade protocol
89
90 terminate(_Reason, _Req, _State) ->
91 626 ok.
92
93 allowed_methods(Req, #state{bindings=Bindings, opts=Opts}=State) ->
94 626 case call(handle_options, [Bindings, Opts], State) of
95 no_call ->
96
:-(
allowed_methods_from_exports(Req, State);
97 Methods ->
98 626 allowed_methods_from_module(Methods, Req, State)
99 end.
100
101 content_types_provided(Req, State) ->
102 624 CTP = [{{<<"application">>, <<"json">>, '*'}, to_json},
103 {{<<"application">>, <<"xml">>, '*'}, to_xml}],
104 624 {CTP, Req, State}.
105
106 content_types_accepted(Req, State) ->
107 32 CTA = [{{<<"application">>, <<"json">>, '*'}, from_json}],
108 32 {CTA, Req, State}.
109
110 delete_resource(Req, State) ->
111 21 handle_delete(Req, State).
112
113 %%--------------------------------------------------------------------
114 %% content_types_provided/2 callbacks
115 %%--------------------------------------------------------------------
116 to_json(Req, State) ->
117 571 handle_get(mongoose_api_json, Req, State).
118
119 to_xml(Req, State) ->
120
:-(
handle_get(mongoose_api_xml, Req, State).
121
122 %%--------------------------------------------------------------------
123 %% content_types_accepted/2 callbacks
124 %%--------------------------------------------------------------------
125 from_json(Req, State) ->
126 32 handle_unsafe(mongoose_api_json, Req, State).
127
128 %%--------------------------------------------------------------------
129 %% HTTP verbs handlers
130 %%--------------------------------------------------------------------
131 handle_get(Serializer, Req, #state{opts=Opts, bindings=Bindings}=State) ->
132 571 Result = call(handle_get, [Bindings, Opts], State),
133 571 handle_result(Result, Serializer, Req, State).
134
135 handle_unsafe(Deserializer, Req, State) ->
136 32 Method = cowboy_req:method(Req),
137 32 {ok, Body, Req1} = cowboy_req:read_body(Req),
138 32 case Deserializer:deserialize(Body) of
139 {ok, Data} ->
140 31 handle_unsafe(Method, Data, Req1, State);
141 {error, _Reason} ->
142 1 error_response(bad_request, Req1, State)
143 end.
144
145 handle_unsafe(Method, Data, Req, #state{opts=Opts, bindings=Bindings}=State) ->
146 31 case method_callback(Method) of
147 not_implemented ->
148
:-(
error_response(not_implemented, Req, State);
149 Callback ->
150 31 Result = call(Callback, [Data, Bindings, Opts], State),
151 31 handle_result(Result, Req, State)
152 end.
153
154 handle_delete(Req, #state{opts=Opts, bindings=Bindings}=State) ->
155 21 Result = call(handle_delete, [Bindings, Opts], State),
156 21 handle_result(Result, Req, State).
157
158 %%--------------------------------------------------------------------
159 %% Helpers
160 %%--------------------------------------------------------------------
161 handle_result({ok, Result}, Serializer, Req, State) ->
162 568 serialize(Result, Serializer, Req, State);
163 handle_result(Other, _Serializer, Req, State) ->
164 3 handle_result(Other, Req, State).
165
166 handle_result(ok, Req, State) ->
167 50 {true, Req, State};
168 handle_result({error, Error}, Req, State) ->
169 5 error_response(Error, Req, State);
170 handle_result(no_call, Req, State) ->
171
:-(
error_response(not_implemented, Req, State).
172
173 allowed_methods_from_module(Methods, Req, State) ->
174 626 Methods1 = case lists:member(get, Methods) of
175 573 true -> [head | Methods];
176 53 false -> Methods
177 end,
178 626 Methods2 = [options | Methods1],
179 626 {methods_to_binary(Methods2), Req, State}.
180
181 allowed_methods_from_exports(Req, #state{handler=Handler}=State) ->
182
:-(
Exports = Handler:module_info(exports),
183
:-(
Methods = lists:foldl(fun collect_allowed_methods/2, [options], Exports),
184
:-(
{methods_to_binary(Methods), Req, State}.
185
186 collect_allowed_methods({handle_get, 2}, Acc) ->
187
:-(
[head, get | Acc];
188 collect_allowed_methods({handle_post, 3}, Acc) ->
189
:-(
[post | Acc];
190 collect_allowed_methods({handle_put, 3}, Acc) ->
191
:-(
[put | Acc];
192 collect_allowed_methods({handle_delete, 2}, Acc) ->
193
:-(
[delete | Acc];
194 collect_allowed_methods(_Other, Acc) ->
195
:-(
Acc.
196
197 serialize(Data, Serializer, Req, State) ->
198 568 {Serializer:serialize(Data), Req, State}.
199
200 call(Function, Args, #state{handler=Handler}) ->
201 1249 try
202 1249 apply(Handler, Function, Args)
203 catch error:undef ->
204
:-(
no_call
205 end.
206
207 %%--------------------------------------------------------------------
208 %% Error responses
209 %%--------------------------------------------------------------------
210 error_response(Code, Req, State) when is_integer(Code) ->
211 6 Req1 = cowboy_req:reply(Code, Req),
212 6 {stop, Req1, State};
213 error_response(Reason, Req, State) ->
214 6 error_response(error_code(Reason), Req, State).
215
216 1 error_code(bad_request) -> 400;
217 4 error_code(not_found) -> 404;
218
:-(
error_code(conflict) -> 409;
219 1 error_code(unprocessable) -> 422;
220
:-(
error_code(not_implemented) -> 501.
221
222 methods_to_binary(Methods) ->
223 626 [method_to_binary(Method) || Method <- Methods].
224
225 573 method_to_binary(get) -> <<"GET">>;
226
:-(
method_to_binary(post) -> <<"POST">>;
227 53 method_to_binary(put) -> <<"PUT">>;
228 53 method_to_binary(delete) -> <<"DELETE">>;
229
:-(
method_to_binary(patch) -> <<"PATCH">>;
230 626 method_to_binary(options) -> <<"OPTIONS">>;
231 573 method_to_binary(head) -> <<"HEAD">>.
232
233
:-(
method_callback(<<"POST">>) -> handle_post;
234 31 method_callback(<<"PUT">>) -> handle_put;
235
:-(
method_callback(_Other) -> not_implemented.
Line Hits Source