lib/ibanity/request.ex

defmodule Ibanity.Request do
  @moduledoc """
  Abstraction layer that eases the construction of an HTTP request.

  Most of the functions come in two flavors. Those with a `Ibanity.Request` as first argument modify return a modified version of it,
  and those without one first create a base `Ibanity.Request` and modify it afterwards.
  The only purpose of this mechanism is to ease the construction of a request.

  Note that all functions of this module return a `Ibanity.Request`.
  None of them can directly fail, and they can therefore be used with the pipe operator.
  """

  @base_headers [
    Accept: "application/json",
    "Content-Type": "application/json"
  ]

  defstruct application: :default,
            headers: @base_headers,
            attributes: %{},
            meta: %{},
            idempotency_key: nil,
            customer_access_token: nil,
            resource_type: nil,
            resource_ids: [],
            page: %{},
            query_params: %{}

  @doc """
  Creates a new request and sets the application name

  See `application/2`.
  """
  def application(name), do: application(%__MODULE__{}, name)

  @doc """
  Sets the request's application name
  """
  def application(%__MODULE__{} = request, name) when is_atom(name) do
    %__MODULE__{request | application: name}
  end

  @doc """
  Creates a new request and adds a header to it.

  Same as `header(%Request{}, header, value)`
  """
  def header(header, value), do: header(%__MODULE__{}, header, value)

  @doc """
  Adds a header to a request. Override existing header with the same name if it is present.

  ## Examples

      iex> header(%Request{}, :"X-Http-Dummy", "1708ef66-d37d-4ce0-85d8-6c062863418a")
      %Ibanity.Request{
        headers: [
          Accept: "application/json",
          "Content-Type": "application/json",
          "X-Http-Dummy": "1708ef66-d37d-4ce0-85d8-6c062863418a"
        ],
        ...
      }

      iex> %Request{headers: ["X-Http-Dummy": "1708ef66-d37d-4ce0-85d8-6c062863418a"]}
      ...> |> header(:"X-Http-Dummy", "396c66d5-daf6-48ff-ba4a-58b9be319ec5")
      %Ibanity.Request{
        headers: ["X-Http-Dummy": "396c66d5-daf6-48ff-ba4a-58b9be319ec5"],
        ...
      }
  """
  def header(%__MODULE__{} = request, header, value) do
    %__MODULE__{request | headers: Keyword.put(request.headers, header, value)}
  end

  @doc """
  Creates a new request and adds headers to it.

  Same as `headers(%Request{}, headers)`
  """
  def headers(headers), do: headers(%__MODULE__{}, headers)

  @doc """
  Adds multiple headers to a request, all at once.
  Override existing headers with the same name if they are present.

  ## Examples

      iex> headers(%Request{}, ["X-Dummy1": "1708ef66", "X-Dummy2": "28207dbe"])
      %Ibanity.Request{
        headers: [
          Accept: "application/json",
          "Content-Type": "application/json",
          "X-Dummy1": "1708ef66",
          "X-Dummy2": "28207dbe"
        ],
        ...
      }

      iex> %Request{headers: ["X-Dummy1": "1708ef66", "X-Dummy2": "28207dbe"]}
      ...> |> headers(["X-Dummy1": "1708ef66", "X-Dummy3": "5127d068"])
      %Ibanity.Request{
        headers: [
          "X-Dummy2": "28207dbe",
          "X-Dummy1": "1708ef66",
          "X-Dummy3": "5127d068"
        ],
        ...
      }
  """
  def headers(%__MODULE__{} = request, headers) when is_list(headers) do
    %__MODULE__{request | headers: Keyword.merge(request.headers, headers)}
  end

  @doc """
  Creates a new `Ibanity.Request` and sets the [idempotency key](https://documentation.ibanity.com/api#idempotency) to it
  """
  def idempotency_key(key), do: idempotency_key(%__MODULE__{}, key)

  @doc """
  Sets the [idempotency key](https://documentation.ibanity.com/api#idempotency) to the request
  """
  def idempotency_key(%__MODULE__{} = request, key) when is_binary(key) do
    %__MODULE__{request | idempotency_key: key}
  end

  @doc """
  Creates a new request and adds the query params and their corresponding values to it, all at once.
  Overrides existing query params with the same name
  """
  def query_params(query_params), do: query_params(%__MODULE__{}, query_params)

  @doc """
  Adds the query params and their corresponding values to the request, all at once.
  Overrides existing query params with the same name
  """
  def query_params(%__MODULE__{} = request, query_params) do
    %__MODULE__{request | query_params: Map.merge(request.query_params, Enum.into(query_params, %{}))}
  end

  @doc """
  Creates a new request and sets the [customer access token](https://documentation.ibanity.com/api#customer-access-token) to it
  """
  def customer_access_token(token) when is_binary(token),
    do: customer_access_token(%__MODULE__{}, token)

  def customer_access_token(%Ibanity.Xs2a.CustomerAccessToken{} = access),
    do: customer_access_token(access.token)

  @doc """
  Sets the [customer access token](https://documentation.ibanity.com/api#customer-access-token) to the request
  """
  def customer_access_token(%__MODULE__{} = request, %Ibanity.Xs2a.CustomerAccessToken{} = access) do
    customer_access_token(request, access.token)
  end

  @doc """
  Sets the [customer access token](https://documentation.ibanity.com/api#customer-access-token) to the request
  """
  def customer_access_token(%__MODULE__{} = request, token) do
    %__MODULE__{request | customer_access_token: token}
  end

  @doc """
  Creates a new request and adds the attribute and its value to it
  """
  def attribute(attribute, value), do: attribute(%__MODULE__{}, attribute, value)

  @doc """
  Adds an attribute to a request. Overrides existing attribute with the same name
  """
  def attribute(%__MODULE__{} = request, attribute, value) do
    %__MODULE__{request | attributes: Map.put(request.attributes, attribute, value)}
  end

  @doc """
  Creates a new request and adds the attributes and their corresponding value to it, all at once.
  Override existing attributes with the same name
  """
  def attributes(attributes), do: attributes(%__MODULE__{}, attributes)

  @doc """
  Adds the attributes and their corresponding value to the request, all at once.
  Override existing attributes with the same name
  """
  def attributes(%__MODULE__{} = request, attributes) when is_list(attributes) do
    %__MODULE__{request | attributes: Map.merge(request.attributes, Enum.into(attributes, %{}))}
  end

  def meta(%__MODULE__{} = request, meta) do
    %__MODULE__{request | meta: meta}
  end

  @doc """
  Creates a new request and sets the resource type to it.
  """
  def resource_type(type), do: resource_type(%__MODULE__{}, type)

  @doc """
  Sets the resource type to the request.
  """
  def resource_type(%__MODULE__{} = request, type) do
    %__MODULE__{request | resource_type: type}
  end

  @doc """
  Creates a new request and sets the `:id` URI identifier.
  It is equivalent to `id(:id, value)`.
  """
  def id(value), do: id(%__MODULE__{}, :id, value)

  @doc """
  Sets the `:id` URI identifier.
  It is equivalent to `id(request, :id, value)`.
  """
  def id(%__MODULE__{} = request, value), do: id(request, :id, value)

  @doc """
  Creates a new request and adds an URI identifier to it.
  """
  def id(name, value), do: id(%__MODULE__{}, name, value)

  @doc """
  Sets the URI identifier to its corresponding value. Overrides existing value if identifier's already present
  """
  def id(%__MODULE__{} = request, name, value) do
    %__MODULE__{request | resource_ids: Keyword.put(request.resource_ids, name, value)}
  end

  @doc """
  Creates a new request and add multiple URI identifiers at once.
  """
  def ids(ids), do: ids(%__MODULE__{}, ids)

  @doc """
  Sets URI template identifiers to their corresponding values. Overrides existing values if identifiers are already present
  """
  def ids(%__MODULE__{} = request, ids) when is_list(ids) do
    %__MODULE__{request | resource_ids: Keyword.merge(request.resource_ids, ids)}
  end

  def limit(max), do: limit(%__MODULE__{}, max)

  @doc """
  Sets the maximum number of items to fetch at once. See [https://documentation.ibanity.com/api#pagination](https://documentation.ibanity.com/api#pagination)
  """
  def limit(%__MODULE__{} = request, max) when is_integer(max) do
    %__MODULE__{request | page: Map.merge(request.page, %{limit: max})}
  end

  def page_number(value), do: page_number(%__MODULE__{}, value)

  @doc """
  Sets the page of results to fetch using page-based pagination. See [https://documentation.ibanity.com/api#page-based-pagination](https://documentation.ibanity.com/api#page-based-pagination)
  """
  def page_number(%__MODULE__{} = request, value) when is_integer(value) do
    %__MODULE__{request | page: Map.merge(request.page, %{number: value})}
  end

  def page_size(value), do: page_size(%__MODULE__{}, value)

  @doc """
  Sets the maximum number of results to fetch per page. See [https://documentation.ibanity.com/api#page-based-pagination](https://documentation.ibanity.com/api#page-based-pagination)
  """
  def page_size(%__MODULE__{} = request, value) when is_integer(value) do
    %__MODULE__{request | page: Map.merge(request.page, %{size: value})}
  end

  def before_id(id), do: before_id(%__MODULE__{}, id)

  @doc """
  Sets the pagination cursor to the given id. See [https://documentation.ibanity.com/api#pagination](https://documentation.ibanity.com/api#pagination)
  """
  def before_id(%__MODULE__{} = request, id) do
    %__MODULE__{request | page: Map.merge(request.page, %{before: id})}
  end

  def after_id(id), do: after_id(%__MODULE__{}, id)

  @doc """
  Sets the pagination cursor to the given id. See [https://documentation.ibanity.com/api#pagination](https://documentation.ibanity.com/api#pagination)
  """
  def after_id(%__MODULE__{} = request, id) do
    %__MODULE__{request | page: Map.merge(request.page, %{after: id})}
  end

  @doc """
  Checks if the request contains a specific id.
  """
  def has_id?(%__MODULE__{} = request, id) do
    Keyword.has_key?(request.resource_ids, id)
  end

  @doc """
  Fetches an id from the request
  """
  def get_id(%__MODULE__{} = request, id) do
    Keyword.get(request.resource_ids, id)
  end

  @doc """
  Checks if the request contains a specific header.
  """
  def has_header?(%__MODULE__{} = request, header) do
    Keyword.has_key?(request.headers, header)
  end

  @doc """
  Fetches a header from the request
  """
  def get_header(%__MODULE__{} = request, header) do
    Keyword.get(request.headers, header)
  end

  def has_customer_access_token?(%__MODULE__{} = request) do
    not is_nil(request.customer_access_token)
  end
end