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(cowboy_rest). |
19 |
|
|
20 |
|
%% ejabberd_cowboy callbacks |
21 |
|
-export([cowboy_router_paths/2]). |
22 |
|
|
23 |
|
%% cowboy_rest callbacks |
24 |
|
-export([init/2, |
25 |
|
terminate/3, |
26 |
|
allowed_methods/2, |
27 |
|
content_types_provided/2, |
28 |
|
content_types_accepted/2, |
29 |
|
delete_resource/2]). |
30 |
|
|
31 |
|
-export([to_xml/2, |
32 |
|
to_json/2, |
33 |
|
from_json/2]). |
34 |
|
|
35 |
|
-ignore_xref([behaviour_info/1, cowboy_router_paths/2, from_json/2, to_json/2, to_xml/2]). |
36 |
|
|
37 |
|
-record(state, {handler, opts, bindings}). |
38 |
|
|
39 |
|
-type prefix() :: string(). |
40 |
|
-type route() :: {string(), options()}. |
41 |
|
-type routes() :: [route()]. |
42 |
|
-type bindings() :: proplists:proplist(). |
43 |
|
-type options() :: [any()]. |
44 |
|
-type method() :: get | post | put | patch | delete. |
45 |
|
-type methods() :: [method()]. |
46 |
|
-type response() :: ok | {ok, any()} | {error, atom()}. |
47 |
|
-export_type([prefix/0, routes/0, route/0, bindings/0, options/0, response/0, methods/0]). |
48 |
|
|
49 |
|
-callback prefix() -> prefix(). |
50 |
|
-callback routes() -> routes(). |
51 |
|
-callback handle_options(bindings(), options()) -> methods(). |
52 |
|
-callback handle_get(bindings(), options()) -> response(). |
53 |
|
-callback handle_post(term(), bindings(), options()) -> response(). |
54 |
|
-callback handle_put(term(), bindings(), options()) -> response(). |
55 |
|
-callback handle_delete(bindings(), options()) -> response(). |
56 |
|
|
57 |
|
%%-------------------------------------------------------------------- |
58 |
|
%% ejabberd_cowboy callbacks |
59 |
|
%%-------------------------------------------------------------------- |
60 |
|
cowboy_router_paths(Base, Opts) -> |
61 |
120 |
Handlers = gen_mod:get_opt(handlers, Opts, []), |
62 |
120 |
lists:flatmap(pa:bind(fun register_handler/2, Base), Handlers). |
63 |
|
|
64 |
|
register_handler(Base, Handler) -> |
65 |
240 |
[{[Base, Handler:prefix(), Path], ?MODULE, [{handler, Handler}|Opts]} |
66 |
240 |
|| {Path, Opts} <- Handler:routes()]. |
67 |
|
|
68 |
|
%%-------------------------------------------------------------------- |
69 |
|
%% cowboy_rest callbacks |
70 |
|
%%-------------------------------------------------------------------- |
71 |
|
init(Req, Opts) -> |
72 |
626 |
case lists:keytake(handler, 1, Opts) of |
73 |
|
{value, {handler, Handler}, Opts1} -> |
74 |
626 |
Bindings = maps:to_list(cowboy_req:bindings(Req)), |
75 |
626 |
State = #state{handler=Handler, opts=Opts1, bindings=Bindings}, |
76 |
626 |
{cowboy_rest, Req, State}; % upgrade protocol |
77 |
|
false -> |
78 |
:-( |
erlang:throw(no_handler_defined) |
79 |
|
end. |
80 |
|
|
81 |
|
terminate(_Reason, _Req, _State) -> |
82 |
626 |
ok. |
83 |
|
|
84 |
|
allowed_methods(Req, #state{bindings=Bindings, opts=Opts}=State) -> |
85 |
626 |
case call(handle_options, [Bindings, Opts], State) of |
86 |
|
no_call -> |
87 |
:-( |
allowed_methods_from_exports(Req, State); |
88 |
|
Methods -> |
89 |
626 |
allowed_methods_from_module(Methods, Req, State) |
90 |
|
end. |
91 |
|
|
92 |
|
content_types_provided(Req, State) -> |
93 |
624 |
CTP = [{{<<"application">>, <<"json">>, '*'}, to_json}, |
94 |
|
{{<<"application">>, <<"xml">>, '*'}, to_xml}], |
95 |
624 |
{CTP, Req, State}. |
96 |
|
|
97 |
|
content_types_accepted(Req, State) -> |
98 |
32 |
CTA = [{{<<"application">>, <<"json">>, '*'}, from_json}], |
99 |
32 |
{CTA, Req, State}. |
100 |
|
|
101 |
|
delete_resource(Req, State) -> |
102 |
21 |
handle_delete(Req, State). |
103 |
|
|
104 |
|
%%-------------------------------------------------------------------- |
105 |
|
%% content_types_provided/2 callbacks |
106 |
|
%%-------------------------------------------------------------------- |
107 |
|
to_json(Req, State) -> |
108 |
571 |
handle_get(mongoose_api_json, Req, State). |
109 |
|
|
110 |
|
to_xml(Req, State) -> |
111 |
:-( |
handle_get(mongoose_api_xml, Req, State). |
112 |
|
|
113 |
|
%%-------------------------------------------------------------------- |
114 |
|
%% content_types_accepted/2 callbacks |
115 |
|
%%-------------------------------------------------------------------- |
116 |
|
from_json(Req, State) -> |
117 |
32 |
handle_unsafe(mongoose_api_json, Req, State). |
118 |
|
|
119 |
|
%%-------------------------------------------------------------------- |
120 |
|
%% HTTP verbs handlers |
121 |
|
%%-------------------------------------------------------------------- |
122 |
|
handle_get(Serializer, Req, #state{opts=Opts, bindings=Bindings}=State) -> |
123 |
571 |
Result = call(handle_get, [Bindings, Opts], State), |
124 |
571 |
handle_result(Result, Serializer, Req, State). |
125 |
|
|
126 |
|
handle_unsafe(Deserializer, Req, State) -> |
127 |
32 |
Method = cowboy_req:method(Req), |
128 |
32 |
{ok, Body, Req1} = cowboy_req:read_body(Req), |
129 |
32 |
case Deserializer:deserialize(Body) of |
130 |
|
{ok, Data} -> |
131 |
31 |
handle_unsafe(Method, Data, Req1, State); |
132 |
|
{error, _Reason} -> |
133 |
1 |
error_response(bad_request, Req1, State) |
134 |
|
end. |
135 |
|
|
136 |
|
handle_unsafe(Method, Data, Req, #state{opts=Opts, bindings=Bindings}=State) -> |
137 |
31 |
case method_callback(Method) of |
138 |
|
not_implemented -> |
139 |
:-( |
error_response(not_implemented, Req, State); |
140 |
|
Callback -> |
141 |
31 |
Result = call(Callback, [Data, Bindings, Opts], State), |
142 |
31 |
handle_result(Result, Req, State) |
143 |
|
end. |
144 |
|
|
145 |
|
handle_delete(Req, #state{opts=Opts, bindings=Bindings}=State) -> |
146 |
21 |
Result = call(handle_delete, [Bindings, Opts], State), |
147 |
21 |
handle_result(Result, Req, State). |
148 |
|
|
149 |
|
%%-------------------------------------------------------------------- |
150 |
|
%% Helpers |
151 |
|
%%-------------------------------------------------------------------- |
152 |
|
handle_result({ok, Result}, Serializer, Req, State) -> |
153 |
568 |
serialize(Result, Serializer, Req, State); |
154 |
|
handle_result(Other, _Serializer, Req, State) -> |
155 |
3 |
handle_result(Other, Req, State). |
156 |
|
|
157 |
|
handle_result(ok, Req, State) -> |
158 |
50 |
{true, Req, State}; |
159 |
|
handle_result({error, Error}, Req, State) -> |
160 |
5 |
error_response(Error, Req, State); |
161 |
|
handle_result(no_call, Req, State) -> |
162 |
:-( |
error_response(not_implemented, Req, State). |
163 |
|
|
164 |
|
allowed_methods_from_module(Methods, Req, State) -> |
165 |
626 |
Methods1 = case lists:member(get, Methods) of |
166 |
573 |
true -> [head | Methods]; |
167 |
53 |
false -> Methods |
168 |
|
end, |
169 |
626 |
Methods2 = [options | Methods1], |
170 |
626 |
{methods_to_binary(Methods2), Req, State}. |
171 |
|
|
172 |
|
allowed_methods_from_exports(Req, #state{handler=Handler}=State) -> |
173 |
:-( |
Exports = Handler:module_info(exports), |
174 |
:-( |
Methods = lists:foldl(fun collect_allowed_methods/2, [options], Exports), |
175 |
:-( |
{methods_to_binary(Methods), Req, State}. |
176 |
|
|
177 |
|
collect_allowed_methods({handle_get, 2}, Acc) -> |
178 |
:-( |
[head, get | Acc]; |
179 |
|
collect_allowed_methods({handle_post, 3}, Acc) -> |
180 |
:-( |
[post | Acc]; |
181 |
|
collect_allowed_methods({handle_put, 3}, Acc) -> |
182 |
:-( |
[put | Acc]; |
183 |
|
collect_allowed_methods({handle_delete, 2}, Acc) -> |
184 |
:-( |
[delete | Acc]; |
185 |
|
collect_allowed_methods(_Other, Acc) -> |
186 |
:-( |
Acc. |
187 |
|
|
188 |
|
serialize(Data, Serializer, Req, State) -> |
189 |
568 |
{Serializer:serialize(Data), Req, State}. |
190 |
|
|
191 |
|
call(Function, Args, #state{handler=Handler}) -> |
192 |
1249 |
try |
193 |
1249 |
apply(Handler, Function, Args) |
194 |
|
catch error:undef -> |
195 |
:-( |
no_call |
196 |
|
end. |
197 |
|
|
198 |
|
%%-------------------------------------------------------------------- |
199 |
|
%% Error responses |
200 |
|
%%-------------------------------------------------------------------- |
201 |
|
error_response(Code, Req, State) when is_integer(Code) -> |
202 |
6 |
Req1 = cowboy_req:reply(Code, Req), |
203 |
6 |
{stop, Req1, State}; |
204 |
|
error_response(Reason, Req, State) -> |
205 |
6 |
error_response(error_code(Reason), Req, State). |
206 |
|
|
207 |
1 |
error_code(bad_request) -> 400; |
208 |
4 |
error_code(not_found) -> 404; |
209 |
:-( |
error_code(conflict) -> 409; |
210 |
1 |
error_code(unprocessable) -> 422; |
211 |
:-( |
error_code(not_implemented) -> 501. |
212 |
|
|
213 |
|
methods_to_binary(Methods) -> |
214 |
626 |
[method_to_binary(Method) || Method <- Methods]. |
215 |
|
|
216 |
573 |
method_to_binary(get) -> <<"GET">>; |
217 |
:-( |
method_to_binary(post) -> <<"POST">>; |
218 |
53 |
method_to_binary(put) -> <<"PUT">>; |
219 |
53 |
method_to_binary(delete) -> <<"DELETE">>; |
220 |
:-( |
method_to_binary(patch) -> <<"PATCH">>; |
221 |
626 |
method_to_binary(options) -> <<"OPTIONS">>; |
222 |
573 |
method_to_binary(head) -> <<"HEAD">>. |
223 |
|
|
224 |
:-( |
method_callback(<<"POST">>) -> handle_post; |
225 |
31 |
method_callback(<<"PUT">>) -> handle_put; |
226 |
:-( |
method_callback(_Other) -> not_implemented. |