-module( h1_request ). -author( "Warren Kenny " ). -export( [get/2, get/3, delete/2, delete/3, post/3, post/4] ). -type request() :: httpc:request(). -type param() :: { string(), string() }. -type params() :: [param()]. -type opts() :: h1:opts(). %% %% Perform an authenticated GET request against the specified API endpoint with the given query parameters. %% -spec get( [string()], [ { string(), string() } ], h1:handle() ) -> { error, term() } | { ok, map() }. get( Endpoint, Params, Handle ) -> request( h1:base_url( Handle ), Endpoint, Params, get, undefined, undefined, h1:auth( Handle ), h1:opts( Handle ) ). -spec get( binary() | [string()], h1:handle() ) -> { error, term() } | { ok, map() }. get( Url, Handle ) when is_binary( Url ) -> request( get, { want:string( Url ), auth( h1:auth( Handle ), [] ) }, want:string( Url ), h1:opts( Handle ) ); get( Endpoint, Handle ) -> request( h1:base_url( Handle ), Endpoint, [], get, undefined, undefined, h1:auth( Handle ), h1:opts( Handle ) ). %% %% Perform an authenticated DELETE request against the specified API endpoint with the given query parameters. %% -spec delete( [string()], [ { string(), string() } ], h1:handle() ) -> { error, term() } | { ok, map() }. delete( Endpoint, Params, Handle ) -> request( h1:base_url( Handle ), Endpoint, Params, delete, undefined, undefined, h1:auth( Handle ), h1:opts( Handle ) ). -spec delete( [string()], h1:handle() ) -> { error, term() } | { ok, map() }. delete( Endpoint, Handle ) -> request( h1:base_url( Handle ), Endpoint, [], delete, undefined, undefined, h1:auth( Handle ), h1:opts( Handle ) ). %% %% Perform a POST request to the specified API endpoint with the given query parameters and data. %% -spec post( [string()], [{ string(), string() }], term(), h1:handle() ) -> { error, term() } | { ok, map() }. post( Endpoint, Params, Data, Handle ) -> request( h1:base_url( Handle ), Endpoint, Params, post, jsx:encode( Data ), "application/json", h1:auth( Handle ), h1:opts( Handle ) ). %% %% Perform an authenticated POST request to the specified API endpoint sending the given data. %% -spec post( [string()], term(), h1:handle() ) -> { error, term() } | { ok, map() }. post( Endpoint, Data, Handle ) -> request( h1:base_url( Handle ), Endpoint, [], post, jsx:encode( Data ), "application/json", h1:auth( Handle ), h1:opts( Handle ) ). %% %% @doc Generate an authentication header for inclusion in a request based on the authentication method %% chosen during the gh:init phase %% -spec auth( atom() | { atom(), string() } | { atom(), string(), string() }, [{string(), string()}] ) -> [{ string(), string() }]. auth( anonymous, Headers ) -> Headers; auth( { oauth, Token }, Headers ) -> [{ "Authorization", string:concat( "token ", Token ) } | Headers]; auth( { basic, Username, Password }, Headers ) -> BasicAuth = base64:encode_to_string( lists:append( [ Username, ":", Password ] ) ), [{ "Authorization", string:concat( "Basic ", BasicAuth ) } | Headers]. %% %% @doc Generate a URL by joining a base URL with the provided path and query parameters %% -spec url( string() | binary(), string(), [{ string(), string() }] ) -> string(). url( Url, Path, QueryParams ) when is_list( Url ) -> case lists:last( Url ) of $/ -> url( lists:droplast( Url ), Path, QueryParams ); _ -> url( list_to_binary( Url ), Path, QueryParams ) end; url( <>, Path, QueryParams ) -> url( Url, Path, QueryParams ); url( Url, Path, QueryParams ) -> binary_to_list( iolist_to_binary( lists:droplast( lists:flatten( [ Url, [ [ $/, P ] || P <- Path ], $?, [ [ url:encode( K ), $=, url:encode( V ), $& ] || { K, V } <- QueryParams ] ] ) ) ) ). %% %% @doc Make a request to the HackerOne API %% -spec request( atom(), request(), string(), opts() ) -> { ok, term() } | { error, term() }. request( Method, Request, Url, Opts ) -> case httpc:request( Method, setelement( 1, Request, Url ), [], [{ body_format, binary }] ) of %% Request completed with no content returned { ok, { { _Version, 204, _Reason }, _Headers, _Body } } -> { ok, no_content }; { ok, { { _Version, Status, _Reason }, _Headers, Body } } when Status >= 200 andalso Status < 300 -> { ok, jsx:decode( Body, [ { labels, proplists:get_value(labels, Opts, atom) }, return_maps ] ) }; %% Some other status code { ok, { { _Version, Status, Reason }, _Headers, Body } } -> { error, { Url, Status, Reason, Body } }; %% Request failed { error, Reason } -> { error, Reason } end. -spec request( string(), [string()], params(), atom(), term(), string(), h1:handle(), opts() ) -> { ok, term() } | { error, term() }. request( BaseURL, Endpoint, Params, Method, Body, ContentType, Auth, Opts ) when Method =:= post orelse Method =:= put orelse Method =:= patch -> Url = url( BaseURL, Endpoint, Params ), request( Method, { Url, auth( Auth, [] ), ContentType, Body }, Url, Opts ); request( BaseURL, Endpoint, Params, Method, _Body, _ContentType, Auth, Opts ) when Method =:= get orelse Method =:= delete orelse Method =:= head orelse Method =:= options -> Url = url( BaseURL, Endpoint, Params ), request( Method, { Url, auth( Auth, [] ) }, Url, Opts ).