lib/consul/request.ex

defmodule Consul.Request do
  @moduledoc """
  This module is used to build an HTTP request
  """

  @path_template_regex ~r/{(\+?[^}]+)}/i

  defstruct method: :get, url: "", body: [], query: [], header: []

  @type param_location :: :body | :query | :header
  @type method :: :head | :get | :delete | :trace | :options | :post | :put | :patch
  @type t :: %__MODULE__{
          method: method(),
          url: String.t(),
          body: keyword(),
          query: keyword(),
          header: keyword()
        }

  @spec new() :: Consul.Request.t()
  def new do
    %__MODULE__{}
  end

  @spec method(Consul.Request.t()) :: {:ok, atom()} | :error
  def method(request), do: Map.fetch(request, :method)

  @doc """
  Specify the request method when building a request

  ## Parameters

  *   `request` (*type:* `Consul.Request.t`) - Collected request options
  *   `m` (*type:* `String`) - Request method

  ## Returns

  *   `Consul.Request.t`
  """
  @spec method(Consul.Request.t(), atom()) :: Consul.Request.t()
  def method(request, m) do
    %{request | method: m}
  end

  @spec url(Consul.Request.t()) :: {:ok, String.t()} | :error
  def url(request), do: Map.fetch(request, :url)

  @doc """
  Specify the request URL when building a request

  ## Parameters

  *   `request` (*type:* `Consul.Request.t`) - Collected request options
  *   `u` (*type:* `String`) - Request URL

  ## Returns

  *   `Consul.Request.t`
  """
  @spec url(Consul.Request.t(), String.t(), map()) :: Consul.Request.t()
  def url(request, u, replacements) do
    url(request, replace_path_template_vars(u, replacements))
  end

  def url(request, u) do
    Map.put(request, :url, u)
  end

  defp replace_path_template_vars(u, replacements) do
    Regex.replace(@path_template_regex, u, fn _, var -> replacement_value(var, replacements) end)
  end

  defp replacement_value("+" <> name, replacements) do
    URI.decode(replacement_value(name, replacements))
  end

  defp replacement_value(name, replacements) do
    replacements
    |> Map.get(name, "")
    |> to_string
  end

  @doc """
  Add optional parameters to the request

  ## Parameters

  * `request` (*type:* `Consul.Request.t`) - Collected request options
  * `definitions` (*type:* `Map`) - Map of parameter name to parameter location
  * `options` (*type:* `keyword()`) - The provided optional parameters

  ## Returns

  * `Consul.Request.t`
  """
  @spec add_optional_params(
          Consul.Request.t(),
          %{optional(atom()) => param_location()},
          keyword()
        ) :: Consul.Request.t()
  def add_optional_params(request, _, []), do: request

  def add_optional_params(request, definitions, [{key, value} | tail]) do
    case definitions do
      %{^key => location} ->
        request
        |> add_param(location, key, value)
        |> add_optional_params(definitions, tail)

      _ ->
        add_optional_params(request, definitions, tail)
    end
  end

  @doc """
  Add optional parameters to the request

  ## Parameters

  * `request` (*type:* `Consul.Request.t`) - Collected request options
  * `location` (*type:* `atom()`) - Where to put the parameter
  * `key` (*type:* `atom()`) - The name of the parameter
  * `value` (*type:* `any()`) - The value of the parameter

  ## Returns

  *   `Consul.Request.t`
  """
  @spec add_param(Consul.Request.t(), param_location(), atom(), any()) ::
          Consul.Request.t()
  def add_param(request, :query, key, values) when is_list(values) do
    Enum.reduce(values, request, fn value, req ->
      add_param(req, :query, key, value)
    end)
  end

  def add_param(request, location, key, value) do
    Map.update!(request, location, &(&1 ++ [{key, value}]))
  end
end