lib/google_api/gax/request.ex

# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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

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

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

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

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

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

  @doc """
  Specify the library version when building a request

  ## Parameters

  *   `request` (*type:* `GoogleApi.Gax.Request.t`) - Collected request options
  *   `version` (*type:* `String`) - Library version

  ## Returns

  *   `GoogleApi.Gax.Request.t`
  """
  @spec library_version(GoogleApi.Gax.Request.t(), String.t()) :: GoogleApi.Gax.Request.t()
  def library_version(request, version) do
    %{request | library_version: version}
  end

  @spec method(GoogleApi.Gax.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:* `GoogleApi.Gax.Request.t`) - Collected request options
  *   `m` (*type:* `String`) - Request method

  ## Returns

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

  @spec url(GoogleApi.Gax.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:* `GoogleApi.Gax.Request.t`) - Collected request options
  *   `u` (*type:* `String`) - Request URL

  ## Returns

  *   `GoogleApi.Gax.Request.t`
  """
  @spec url(GoogleApi.Gax.Request.t(), String.t(), map()) :: GoogleApi.Gax.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:* `GoogleApi.Gax.Request.t`) - Collected request options
  *   `definitions` (*type:* `Map`) - Map of parameter name to parameter location
  *   `options` (*type:* `keyword()`) - The provided optional parameters

  ## Returns

  *   `GoogleApi.Gax.Request.t`
  """
  @spec add_optional_params(
          GoogleApi.Gax.Request.t(),
          %{optional(atom()) => param_location()},
          keyword()
        ) :: GoogleApi.Gax.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:* `GoogleApi.Gax.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

  *   `GoogleApi.Gax.Request.t`
  """
  @spec add_param(GoogleApi.Gax.Request.t(), param_location(), atom(), any()) ::
          GoogleApi.Gax.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