lib/lob/client.ex

defmodule Lob.Client do
  @moduledoc """
  Client responsible for making requests to Lob and handling the responses.
  """

  alias HTTPoison.Error
  alias Poison.Parser

  use HTTPoison.Base

  @client_version Mix.Project.config[:version]

  @type client_response :: {:ok, map, list} | {:error, any}

  defmodule MissingAPIKeyError do
    @moduledoc """
    Exception for when a request is made without an API key.
    """

    defexception message: """
                   The api_key setting is required to make requests to Lob.
                   Please configure :api_key in config.exs or set the API_KEY
                   environment variable.

                   config :lob_elixir, api_key: API_KEY
                 """
  end

  @spec client_version :: String.t
  def client_version, do: @client_version

  @spec api_key(atom) :: String.t
  def api_key(env_key \\ :api_key) do
    case Confex.get_env(:lob_elixir, env_key, System.get_env("API_KEY")) || :not_found do
      :not_found -> raise MissingAPIKeyError
      value -> value
    end
  end

  @spec api_version :: String.t | nil
  def api_version,
    do: Application.get_env(:lob_elixir, :api_version, System.get_env("LOB_API_VERSION"))

  # #########################
  # HTTPoison.Base callbacks
  # #########################

  def process_request_headers(headers) do
    api_version()
    |> default_headers()
    |> Map.merge(Map.new(headers))
    |> Enum.into([])
  end

  def process_response_body(body) do
    Parser.parse!(body, %{keys: :atoms})
  end

  # #########################
  # Client API
  # #########################

  @spec get_request(String.t, HTTPoison.Base.headers) :: client_response
  def get_request(url, headers \\ []) do
    url
    |> get(headers, build_options())
    |> handle_response
  end

  @spec post_request(<<_::64, _::_*8>>, {:multipart, [any()]}, [{binary(), binary()}]) :: client_response
  def post_request(url, body, headers \\ []) do
    url
    |> post(body, headers, build_options())
    |> handle_response
  end

  @spec post_request_binary(<<_::64, _::_*8>>, binary(), [{binary(), binary()}]) :: client_response
  def post_request_binary(url, body, headers \\ []) do
    url
    |> post(body, headers, build_options())
    |> handle_response
  end

  @spec delete_request(String.t, HTTPoison.Base.headers) :: client_response
  def delete_request(url, headers \\ []) do
    url
    |> delete(headers, build_options())
    |> handle_response
  end

  # #########################
  # Response handlers
  # #########################

  @spec handle_response({:ok, map} | {:error, Error.t}) :: client_response
  defp handle_response({:ok, %{body: body, headers: headers, status_code: code}})
       when code >= 200 and code < 300 do
    {:ok, body, headers}
  end

  defp handle_response({:ok, %{body: body}}) do
    {:error, body.error}
  end

  defp handle_response({:error, error = %Error{}}) do
    {:error, %{message: Error.message(error)}}
  end

  @spec build_options(String.t) :: Keyword.t
  defp build_options(api_key \\ api_key()) do
    [hackney: [basic_auth: {api_key, ""}], recv_timeout: :infinity]
  end

  @spec default_headers(String.t | nil) :: %{String.t => String.t}
  defp default_headers(nil), do: %{"User-Agent" => "Lob/v1 ElixirBindings/#{client_version()}"}

  defp default_headers(api_version),
    do: %{
      "User-Agent" => "Lob/v1 ElixirBindings/#{client_version()}",
      "Lob-Version" => api_version
    }
end