lib/ept_sdk/resource.ex

defmodule EPTSDK.Resource do
  @moduledoc """
  The basic shape of all resource requests, sliced into various common actions against resources.
  """

  defmacro with_list() do
    quote location: :keep do
      @doc """
      Fetches all `%#{Kernel.inspect(__MODULE__)}{}`.

      The `options` argument can be:

        - `fields`, a map of filds to return for each resource type i.e. `fields: %{#{@resource_type}: ["id"]}`
        - `include`, a list of relationship chains for the response to return i.e. `include: ["#{@resource_type}.merchant_account"]`
        - `sort`, ... i.e. `sort: ["-name"]`
        - `filter`, ... i.e. `fields: %{name: "John"}`
      """

      def list(client, options \\ [])
          when is_struct(client, EPTSDK) do
        client
        |> EPTSDK.get("#{@path}", options)
        |> EPTSDK.update_client_from_request(client)
        |> EPTSDK.Resource.from_payload()
      end
    end
  end

  defmacro with_show() do
    quote location: :keep do
      @doc """
      Fetches a `%#{Kernel.inspect(__MODULE__)}{}` by `record` or by `id`.

      The `options` argument can be:

        - `fields`, a map of filds to return for each resource type i.e. `fields: %{#{@resource_type}: ["id"]}`
        - `include`, a list of relationship chains for the response to return i.e. `include: ["#{@resource_type}.merchant_account"]`
      """

      def show(_, _, options \\ [])

      def show(%EPTSDK{} = client, %__MODULE__{id: id}, options),
        do: show(client, id, options)

      def show(%EPTSDK{} = client, id, options) when is_binary(id) do
        client
        |> EPTSDK.get("#{@path}/#{id}", options)
        |> EPTSDK.update_client_from_request(client)
        |> EPTSDK.Resource.from_payload()
      end
    end
  end

  defmacro with_create() do
    quote location: :keep do
      @doc """
      Creates an new `%#{Kernel.inspect(__MODULE__)}{}}` with `attributes:` and `relationships:`.

      The `options` argument can also have:

        - `fields:`, a map of filds to return for each resource type i.e. `fields: %{#{@resource_type}: ["id"]}`
        - `include:`, a list of relationship chains for the response to return i.e. `include: ["#{@resource_type}.merchant_account"]`
      """
      def create(
            %EPTSDK{} = client,
            options \\ []
          )
          when is_list(options) do
        attributes = Keyword.get(options, :attributes, %{})
        relationships = Keyword.get(options, :relationships, %{})

        client
        |> EPTSDK.post(
          "#{@path}",
          %{
            data: %{
              type: @resource_type,
              attributes: attributes,
              relationships:
                relationships
                |> Enum.map(&EPTSDK.Resource.encode_relation/1)
                |> Map.new()
            }
          },
          options |> Keyword.drop([:attributes, :relationships])
        )
        |> EPTSDK.update_client_from_request(client)
        |> EPTSDK.Resource.from_payload()
      end
    end
  end

  defmacro with_update() do
    quote location: :keep do
      @doc """
      Updates an existing `%#{Kernel.inspect(__MODULE__)}` with `attributes:` and `relationships:`.

      The `options` argument can also have:

        - `fields:`, a map of filds to return for each resource type i.e. `fields: %{#{@resource_type}: ["id"]}`
        - `include:`, a list of relationship chains for the response to return i.e. `include: ["#{@resource_type}.merchant_account"]`
      """
      def update(
            _,
            _,
            options \\ []
          )

      def update(
            %EPTSDK{} = client,
            %__MODULE__{id: id} = record,
            options
          )
          when is_list(options) do
        update(client, id, options)
      end

      def update(
            %EPTSDK{} = client,
            id,
            options
          )
          when is_binary(id) and is_list(options) do
        attributes = Keyword.get(options, :attributes, %{})
        relationships = Keyword.get(options, :relationships, %{})

        client
        |> EPTSDK.patch(
          "#{@path}/#{id}",
          %{
            data: %{
              type: @resource_type,
              attributes: attributes,
              relationships:
                relationships
                |> Enum.map(&EPTSDK.Resource.encode_relation/1)
                |> Map.new()
            }
          },
          options
        )
        |> EPTSDK.update_client_from_request(client)
        |> EPTSDK.Resource.from_payload()
      end
    end
  end

  defmacro with_delete() do
    quote location: :keep do
      def delete(_, _, options \\ [])

      def delete(%EPTSDK{} = client, %__MODULE__{id: id} = record, options),
        do: delete(client, id, options)

      def delete(%EPTSDK{} = client, id, options)
          when is_binary(id) do
        client
        |> EPTSDK.delete("#{@path}/#{id}", options)
        |> EPTSDK.update_client_from_request(client)
        |> EPTSDK.Resource.from_payload()
      end
    end
  end

  def from_payload(
        {:ok,
         %{
           "data" => entity
         } = payload, client}
      )
      when is_map(entity) do
    entity
    |> EPTSDK.Entity.to_struct(payload["links"])
    |> (&{:ok, &1, client}).()
  end

  def from_payload(
        {:ok,
         %{
           "data" => entities
         }, client}
      )
      when is_list(entities) do
    entities
    |> Enum.map(&EPTSDK.Entity.to_struct(&1, nil))
    |> (&{:ok, &1, client}).()
  end

  def from_payload({:ok, nil, client}), do: {:ok, nil, client}
  def from_payload({:error, _} = error), do: error
  def from_payload({:unprocessable_content, _exception, _response} = error), do: error
  def from_payload({:decoding_error, _exception, _response} = error), do: error

  def encode_relation({relation_name, %{id: id, type: type}}) do
    {relation_name, %{"data" => %{"id" => id, "type" => type}}}
  end
end