lib/ash/plug_helpers.ex

if Code.ensure_loaded?(Plug.Conn) do
  defmodule Ash.PlugHelpers do
    @moduledoc """
    Helpers for working with the Plug connection.
    """

    alias Plug.Conn
    require Logger

    defmacrop emit_assign_warning(type) do
      quote do
        Logger.warning(fn ->
          {fun, arity} = __ENV__.function
          file = Path.relative_to_cwd(__ENV__.file)

          """
          Storing the #{unquote(type)} in conn assigns is deprecated.
            #{file}:#{__ENV__.line}: #{inspect(__ENV__.module)}.#{fun}/#{arity}
          """
        end)
      end
    end

    @doc """
    Sets the actor inside the Plug connection.

    The actor is stored inside the [connection's private
    fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).

    ## Example

        iex> actor = build_actor(%{email: "marty@1985.retro"})
        ...> conn = build_conn() |> set_actor(actor)
        %Plug.Conn{private: %{ash: %{actor: %{email: "marty@1985.retro"}}}} = conn

    """
    @spec set_actor(Conn.t(), Ash.Resource.record()) :: Conn.t()
    def set_actor(conn, actor) do
      ash_private =
        conn.private
        |> Map.get(:ash, %{})
        |> Map.put(:actor, actor)

      conn
      |> Conn.put_private(:ash, ash_private)
    end

    @doc """
    Retrieves the actor from the Plug connection.

    The actor is stored inside the [connection's private
    fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).

    ## Deprecation warning

    This function checks to see if the actor is already set in the `@actor`
    assign, and if so will emit a deprecation warning.

    This is to allow apps using the previous method a chance to update.

    Rather than setting the actor in the assigns, please use the `set_actor/2`
    method.

    ## Example

        iex> actor = build_actor(%{email: "marty@1985.retro"})
        ...> conn = build_conn() |> put_private(:ash, %{actor: actor})
        ...> actor = get_actor(conn)
        %{email: "marty@1985.retro"} = actor

        iex> actor = build_actor(%{email: "marty@1985.retro"})
        ...> conn = build_conn() |> assign(:actor, actor)
        ...> actor = get_actor(conn)
        %{email: "marty@1985.retro"} = actor
    """
    @spec get_actor(Conn.t()) :: nil | Ash.Resource.record()
    def get_actor(%{assigns: %{actor: actor}}) when not is_nil(actor) do
      emit_assign_warning(:actor)

      actor
    end

    def get_actor(%{private: %{ash: %{actor: actor}}}), do: actor
    def get_actor(_), do: nil

    @doc """
    Updates the actor inside the Plug connection.

    The actor is stored inside the [connection's private
    fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).

    ## Example

        iex> actor = build_actor(%{email: "marty@1985.retro"})
        ...> conn = build_conn() |> put_private(:ash, %{actor: actor})
        ...> actor = get_actor(conn)
        %{email: "marty@1985.retro"} = actor
        ...> conn = update_actor(conn, fn actor -> Map.put(actor, :name, "Marty Retro") end)
        ...> actor = get_actor(conn)
        %{email: "marty@1985.retro", name: "Marty Retro"} = actor
        ...> conn = update_actor(conn, fn actor -> Map.delete(actor, :email) end)
        ...> actor = get_actor(conn)
        %{name: "Marty Retro"} = actor
    """
    @spec update_actor(Conn.t(), (nil | Ash.Resource.record() -> nil | Ash.Resource.record())) ::
            Conn.t()
    def update_actor(conn, callback) do
      case get_actor(conn) do
        nil ->
          conn

        actor ->
          conn
          |> set_actor(callback.(actor))
      end
    end

    @doc """
    Sets the tenant inside the Plug connection.

    The tenant is stored inside the [connection's private
    fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).

    ## Example

        iex> tenant = build_tenant(%{name: "Deliver-yesterday"})
        ...> conn = build_conn() |> set_tenant(tenant)
        %Plug.Conn{private: %{ash: %{tenant: %{name: "Deliver-yesterday"}}}} = conn
    """
    @spec set_tenant(Conn.t(), Ash.Resource.record()) :: Conn.t()
    def set_tenant(conn, tenant) do
      ash_private =
        conn.private
        |> Map.get(:ash, %{})
        |> Map.put(:tenant, tenant)

      conn
      |> Conn.put_private(:ash, ash_private)
    end

    @doc """
    Retrieves the tenant from the Plug connection.

    The tenant is stored inside the [connection's private
    fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).

    ## Deprecation warning

    This function checks to see if the tenant is already set in the `@tenant`
    assign, and if so will emit a deprecation warning.

    This is to allow apps using the previous method a chance to update.

    Rather than setting the tenant in the assigns, please use the `set_tenant/2`
    method.


    ## Example

        iex> tenant = build_tenant(%{name: "Deliver-yesterday"})
        ...> conn = build_conn() |> put_private(:ash, %{tenant: tenant})
        ...> tenant = get_tenant(conn)
        %{name: "Deliver-yesterday"} = tenant

        iex> tenant = build_tenant(%{name: "Deliver-yesterday"})
        ...> conn = build_conn() |> assign(:tenant, tenant)
        ...> tenant = get_tenant(conn)
        %{name: "Deliver-yesterday"} = tenant
    """
    @spec get_tenant(Conn.t()) :: nil | Ash.Resource.record()
    def get_tenant(%{assigns: %{tenant: tenant}}) when not is_nil(tenant) do
      emit_assign_warning(:tenant)

      tenant
    end

    def get_tenant(%{private: %{ash: %{tenant: tenant}}}), do: tenant
    def get_tenant(_), do: nil

    @doc """
    Sets the context inside the Plug connection.

    Context can be used to store abitrary data about the user, connection, or
    anything else you like that doesn't belong as part of the actor or tenant.

    The context is stored inside the [connection's private
    fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).

    ## Example

        iex> context = %{fraud_score: 0.427}
        ...> conn = build_conn() |> set_context(context)
        %Plug.Conn{private: %{ash: %{context: %{fraud_score: 0.427}}}}

    """
    @spec set_context(Conn.t(), Ash.Resource.record()) :: Conn.t()
    def set_context(conn, context) do
      ash_private =
        conn.private
        |> Map.get(:ash, %{})
        |> Map.put(:context, context)

      conn
      |> Conn.put_private(:ash, ash_private)
    end

    @doc """
    Retrieves the context from the Plug connection.

    The context is stored inside the [connection's private
    fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).

    ## Example

        iex> context = %{fraud_score: 0.427}
        ...> conn = build_conn() |> put_private(:ash, %{context: context})
        ...> context = get_context(conn)
        %{fraud_score: 0.427}
    """
    @spec get_context(Conn.t()) :: nil | Ash.Resource.record()
    def get_context(%{private: %{ash: %{context: context}}}), do: context
    def get_context(_), do: nil

    @doc """
    Updates the context inside the Plug connection.

    The context is stored inside the [connection's private
    fields](https://hexdocs.pm/plug/Plug.Conn.html#module-private-fields).

    ## Example

        iex> context = %{species: "Fythetropozoat"}
        ...> conn = build_conn() |> put_private(:ash, %{context: context})
        ...> context = get_context(conn)
        %{fraud_score: 0.427}
        ...> conn = update_context(conn, fn context -> Map.put(context, :location, "Barnard's Loop") end)
        ...> context = get_context(conn)
        %{species: "Fythetropozoat", location: "Barnard's Loop"}
        ...> conn = update_context(conn, fn context -> Map.delete(context, :fraud_score) end)
        ...> context = get_context(conn)
        %{location: "Barnard's Loop"}
    """
    @spec update_context(Conn.t(), (nil | Ash.Resource.record() -> nil | Ash.Resource.record())) ::
            Conn.t()
    def update_context(conn, callback) do
      case get_context(conn) do
        nil ->
          conn

        context ->
          conn
          |> set_context(callback.(context))
      end
    end
  end
end