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. |