%% MIT License %% Copyright (c) 2021 Viacheslav Katsuba %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal %% in the Software without restriction, including without limitation the rights %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the Software is %% furnished to do so, subject to the following conditions: %% The above copyright notice and this permission notice shall be included in all %% copies or substantial portions of the Software. %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE %% SOFTWARE. -module(shot). %%% ================================================================== %%% API %%% ================================================================== -export([ get/1, put/1, post/1, delete/1, upload/1 ]). %%% ================================================================== %%% Helpers API %%% ================================================================== -export([ get_code/1, get_body/1, redirect/1, catch_redirect/1, http/1, multipart/1, httpc_request/1 ]). %%% ================================================================== %%% Macros %%% ================================================================== -define(BAD_ARG, {error, bad_arg}). -define(BOUNDARY, "----WebKitFormBoundaryShot"). -define(NO_REDIRECT, {autoredirect, false}). %%% ================================================================== %%% API functions %%% ================================================================== %% ------------------------------------------------------------------- %% @doc %% GET request %% @end %% ------------------------------------------------------------------- -spec get(Data :: maps:map() | lists:list()) -> {ok, Result :: tuple()} | {error, Reason :: tuple() | bad_arg}. get(#{u := _} = Data) -> http(maps:merge(#{m => get}, Data)); get(URL) when is_list(URL) -> http(#{m => get, u => URL}); get(_) -> ?BAD_ARG. %% ------------------------------------------------------------------- %% @doc %% PUT request %% @end %% ------------------------------------------------------------------- -spec put(Data :: maps:map() | lists:list()) -> {ok, Result :: tuple()} | {error, Reason :: tuple() | bad_arg}. put(#{u := _} = Data) -> http(maps:merge(#{m => put}, Data)); put(URL) when is_list(URL) -> http(#{m => put, u => URL}); put(_) -> ?BAD_ARG. %% ------------------------------------------------------------------- %% @doc %% POST request %% @end %% ------------------------------------------------------------------- -spec post(Data :: maps:map() | lists:list()) -> {ok, Result :: tuple()} | {error, Reason :: tuple() | bad_arg}. post(#{u := _} = Data) -> http(maps:merge(#{m => post}, Data)); post(URL) when is_list(URL) -> http(#{m => post, u => URL}); post(_) -> ?BAD_ARG. %% ------------------------------------------------------------------- %% @doc %% DELETE request %% @end %% ------------------------------------------------------------------- -spec delete(Data :: maps:map() | lists:list()) -> {ok, Result :: tuple()} | {error, Reason :: tuple() | bad_arg}. delete(#{u := _} = Data) -> http(maps:merge(#{m => delete}, Data)); delete(URL) when is_list(URL) -> http(#{m => delete, u => URL}); delete(_) -> ?BAD_ARG. %% ------------------------------------------------------------------- %% @doc %% UPLOAD request %% @end %% ------------------------------------------------------------------- -spec upload(Data :: maps:map()) -> {ok, Result :: tuple()} | {error, Reason :: tuple() | bad_arg}. upload(#{} = Data) -> multipart(Data); upload(_) -> ?BAD_ARG. %%% ================================================================== %%% Helpers API functions %%% ================================================================== %% ------------------------------------------------------------------- %% @doc %% httpc request with/without headers %% @end %% ------------------------------------------------------------------- -spec http(Params :: maps:map()) -> {ok, Result :: tuple()} | {error, Reason :: tuple()}. http(#{m := _, u := _, b := _, ct := _} = Data) -> httpc_request(Data); http(#{m := _, u := _, h := _} = Data) -> httpc_request(Data); http(#{m := post, u := URL}) -> httpc:request(post, {URL, [], [], []}, [], []); http(#{m := Method, u := URL}) -> httpc:request(Method, {URL, []}, [?NO_REDIRECT], []). %% ------------------------------------------------------------------- %% @doc %% httpc request for upload file %% #{m := method, u := "URL", p := "Path2File", o := Ops, cd := "Content-Disposition", ct := "Content-Type"} %% @end %% ------------------------------------------------------------------- multipart(#{m := M, u := U, p := P, o := O, cd := CD, ct := CT}) when M =:= post; M =:= put -> case file:read_file(P) of {ok, Bin} -> Data = binary_to_list(Bin), ReqBody = form_data(fname(P), Data, CD, CT), ContentType = lists:concat(["multipart/form-data; boundary=", ?BOUNDARY]), ReqHeader = [{"Content-Length", integer_to_list(length(ReqBody))}], httpc:request(M, {U, ReqHeader, ContentType, ReqBody}, O, []); _ -> {error, <<"Cannot find file">>} end; multipart(_) -> {error, <<"Shot Error: The method not allowed">>}. %% ------------------------------------------------------------------- %% @doc %% Returns HTTP response code. %% @end %% ------------------------------------------------------------------- -spec get_code(HttpcResult :: tuple()) -> Code :: pos_integer(). get_code({{_, Code, _}, _, _}) -> Code. %% ------------------------------------------------------------------- %% @doc %% Returns HTTP response body. %% @end %% ------------------------------------------------------------------- -spec get_body(HttpcResult :: tuple()) -> Body :: list(). get_body({_, _, Body}) -> Body. %% ------------------------------------------------------------------- %% @doc %% Catch redirect and GET response from redirect URL %% > {_, Resp1} = shot_utils:http(get, "http://google.com"). %% > {_, Resp2} = shot_utils:redirect(Resp). %% @end %% ------------------------------------------------------------------- -spec redirect(HttpcResult :: tuple()) -> {ok, Result :: tuple()} | {error, Reason :: tuple()}. redirect({{_, 301, _}, Headers, _}) -> http(#{m => get, u => proplists:get_value("location", Headers)}). %% ------------------------------------------------------------------- %% @doc %% Catch redirect URL %% @end %% ------------------------------------------------------------------- -spec catch_redirect(HttpcResult :: tuple()) -> Result :: tuple() | list(). catch_redirect({{_, 301, _}, Headers, _} = Resp) -> proplists:get_value("location", Headers, Resp); catch_redirect(Resp) -> Resp. %%% ================================================================== %%% Internal/Private functions %%% ================================================================== %% ------------------------------------------------------------------- %% @private %% @doc %% Prepare Multipart Form Data %% @end %% ------------------------------------------------------------------- -spec form_data(Fname :: list(), Data :: list(), CD :: list(), CT :: list()) -> Result :: string(). form_data(Fname, Data, CD, CT) -> string:join([ lists:concat(["--", ?BOUNDARY]), lists:concat(["Content-Disposition: ", CD, ";", "name=", Fname]), lists:concat(["Content-Type: ", CT]), "", Data, lists:concat(["--", ?BOUNDARY, "--"]) ], "\r\n"). %% ------------------------------------------------------------------- %% @private %% @doc %% Prepare Multipart Form Data %% @end %% ------------------------------------------------------------------- -spec fname(Path :: list()) -> Fname :: list(). fname(Path) -> binary_to_list(lists:last(binary:split(list_to_binary(Path), <<"/">>, [global]))). %% ------------------------------------------------------------------- %% @doc %% HTTPC request with/without headers %% #{m => get, u => "https://httpbin.org/bearer", h => #{"Authorization" => "Bearer dXNlcjpwYXNz"}} %% @end %% ------------------------------------------------------------------- -spec httpc_request(Params :: maps:map()) -> {ok, Result :: tuple()} | {error, Reason :: tuple()}. httpc_request(#{m := Method, u := URL, h := Headers, b := Body, ct := ContentType}) -> httpc:request(Method, {URL, maps:fold(fun(K, V, Acc) -> [{K, V} | Acc] end, [], Headers), ContentType, Body}, [?NO_REDIRECT], []); httpc_request(#{m := Method, u := URL, b := Body, ct := ContentType}) -> httpc:request(Method, {URL, [], ContentType, Body}, [?NO_REDIRECT], []); httpc_request(#{m := Method, u := URL, h := Headers}) -> httpc:request(Method, {URL, maps:fold(fun(K, V, Acc) -> [{K, V} | Acc] end, [], Headers)}, [?NO_REDIRECT], []).