lib/easypost/request.ex

defmodule EasyPost.Request do
  alias EasyPost.{ Helpers, Response }

  @type t ::
          %__MODULE__{
            attempt: integer,
            body: binary,
            headers: EasyPost.http_headers_t(),
            method: EasyPost.http_method_t(),
            result: any,
            url: String.t()
          }

  defstruct attempt: 0,
            body: nil,
            headers: nil,
            method: nil,
            result: nil,
            url: nil

  @spec send(EasyPost.Operation.t(), EasyPost.Config.t()) :: EasyPost.response_t()
  def send(operation, config) do
    body = Helpers.Body.encode!(operation, config)

    api_key = "#{config.api_key}:"
    api_key = Base.encode64(api_key)

    headers = []
    headers = headers ++ [{ "authorization", "Basic #{api_key}" }]
    headers = headers ++ [{ "content-type", "application/json" }]

    method = operation.method

    url = Helpers.URL.to_string(operation, config)

    request = %__MODULE__{}
    request = Map.put(request, :body, body)
    request = Map.put(request, :headers, headers)
    request = Map.put(request, :method, method)
    request = Map.put(request, :url, url)

    request
    |> dispatch(config)
    |> retry(config)
    |> finish(config)
  end

  defp dispatch(request, config) do
    http_client = config.http_client

    result = http_client.request(request.method, request.url, request.headers, request.body, config.http_client_opts)

    request = Map.put(request, :attempt, request.attempt + 1)
    request = Map.put(request, :result, result)

    request
  end

  defp retry(request, config) do
    max_attempts = Keyword.get(config.retry_opts, :max_attempts, 3)

    if config.retry && max_attempts > request.attempt do
      case request.result do
        { :ok, %{ status_code: status_code } } when status_code >= 500 ->
          dispatch(request, config)
        { :error, _reason } ->
          dispatch(request, config)
        _otherwise ->
          request
      end
    else
      request
    end
  end

  defp finish(request, config) do
    case request.result do
      { :ok, %{ status_code: status_code } = response } when status_code >= 400 ->
        { :error, Response.new(response, config) }
      { :ok, %{ status_code: status_code } = response } when status_code >= 200 ->
        { :ok, Response.new(response, config) }
      otherwise ->
        otherwise
    end
  end
end