Current section
Files
Jump to
Current section
Files
lib/nani/base.ex
defmodule Nani.Base do
use Hayase
alias HTTPoison.{Response, Error}
require Logger
@success_status_codes [200, 201]
@default_headers []
# timeout is 8000ms by default
# recv_timeout is 5000ms by default
@default_options [timeout: 32_000, recv_timeout: 60_000]
@type result_t :: {:ok, String.t() | list | map} | {:error, String.t()}
@type raw_result_t :: {:ok, Response.t()} | {:error, Error.t()}
@spec get(String.t(), map, keyword) :: result_t
def get(url, query_params, opts \\ []) do
url
|> get_raw(query_params, opts)
|> process_response()
end
@spec post(String.t(), map, map | keyword, keyword) :: result_t
def post(url, query_params, post_params, opts \\ []) do
url
|> post_raw(query_params, post_params, opts)
|> process_response()
end
@spec get_raw(String.t(), map, keyword) :: raw_result_t
def get_raw(url, query_params, opts \\ []) do
headers = request_headers(opts)
options = request_options(opts)
url
|> url_with_query(query_params)
|> URI.encode()
|> HTTPoison.get(headers, options)
|> log_response()
end
@spec post_raw(String.t(), map, map | keyword, keyword) :: raw_result_t
def post_raw(url, query_params, post_params, opts \\ []) do
headers = request_headers(opts)
options = request_options(opts)
body = request_body(post_params, headers)
url
|> url_with_query(query_params)
|> URI.encode()
|> HTTPoison.post(body, headers, options)
|> log_response()
end
defp url_with_query(url, query_params) when query_params == %{} do
url
end
# URI.encode_query/1 converts map into a query
# string and percent-encodes both keys and values
#
# URI.decode/1 then percent-decodes this string
defp url_with_query(url, query_params) do
query_string =
query_params
|> URI.encode_query()
|> URI.decode()
"#{url}?#{query_string}"
end
defp request_body(post_params, headers) do
content_type = Map.new(headers)["Content-Type"]
case content_type do
nil ->
raise "Request Content-Type not set"
"application/json" <> _ ->
Jason.encode!(post_params)
"application/x-www-form-urlencoded" <> _ ->
if !Keyword.keyword?(post_params) do
raise "POST params must be a keyword list"
end
{:form, post_params}
_ ->
raise "Request Content-Type not supported: #{content_type}"
end
end
defp request_headers(opts) do
headers = Keyword.get(opts, :headers, [])
@default_headers ++ headers
end
defp request_options(opts) do
options = Keyword.get(opts, :options, [])
Keyword.merge(@default_options, options)
end
defp process_response(result) do
result
|> fmap(&gunzip_response_body/1)
|> fmap(&parse_response_body/1)
|> bind(&extract_response_body/1)
|> process_error()
end
# -----------------------------------------------------------------
# log_response
# -----------------------------------------------------------------
defp log_response({:ok, %Response{status_code: status_code}} = result)
when status_code in @success_status_codes do
{_, response} = result
Logger.debug("HTTP RESPONSE: #{inspect(response)}")
result
end
defp log_response(result) do
{_, response} = result
Logger.error("HTTP RESPONSE: #{inspect(response)}")
result
end
# -----------------------------------------------------------------
# gunzip_response_body
#
# https://github.com/edgurgel/httpoison/issues/81
#
# gzipped body should be decoded transparently by HTTPoison if
# "Content-Encoding: gzip" or "Content-Encoding: x-gzip" header
# is set but this feature is not implemented yet and this header
# may be missing so check if URL leads to gzipped file manually
# -----------------------------------------------------------------
defp gunzip_response_body(%Response{status_code: 200} = response) do
%{body: body, request: %{url: url}} = response
case String.ends_with?(url, "gz") do
true -> %{response | body: :zlib.gunzip(body)}
false -> response
end
end
# -----------------------------------------------------------------
# parse_response_body
# -----------------------------------------------------------------
defp parse_response_body(response) do
%{body: body, headers: headers} = response
content_type =
headers
|> Map.new()
|> Map.fetch!("Content-Type")
case content_type do
"application/json" <> _ ->
%{response | body: Jason.decode!(body)}
_ ->
response
end
end
# -----------------------------------------------------------------
# extract_response_body
# -----------------------------------------------------------------
defp extract_response_body(%Response{body: body, status_code: status_code})
when status_code in @success_status_codes do
{:ok, body}
end
defp extract_response_body(%Response{body: body, status_code: status_code})
when body in ["", " "] do
{:error, "#{status_code}"}
end
defp extract_response_body(%Response{body: body, status_code: status_code}) do
{:error, "#{status_code}: #{body}"}
end
# -----------------------------------------------------------------
# process_error
# -----------------------------------------------------------------
# error can be string, atom, tuple or HTTPoison.Error struct
defp process_error({:error, error}) do
{:error, inspect(error)}
end
defp process_error(result), do: result
end