lib/ash.ex

defmodule Ash do
  @moduledoc """
  General purpose tools for working with Ash and Ash resources.
  """

  for {function, arity} <- Ash.Api.Functions.functions() do
    args = Macro.generate_arguments(arity, __MODULE__)

    defdelegate unquote(function)(unquote_splicing(args)), to: Ash.Api.GlobalInterface

    unless function in Ash.Api.Functions.no_opts_functions() do
      args = Macro.generate_arguments(arity + 1, __MODULE__)

      defdelegate unquote(function)(unquote_splicing(args)), to: Ash.Api.GlobalInterface
    end
  end

  @doc """
  Converts a context map to opts to be passed into an action.
  """
  def context_to_opts(map, add_to \\ []) when is_map(map) do
    add_to
    |> add_if_present(map, :actor)
    |> add_if_present(map, :authorize?)
    |> add_if_present(map, :tracer)
  end

  defp add_if_present(opts, map, key) do
    case Map.fetch(map, key) do
      {:ok, value} -> Keyword.put(opts, key, value)
      :error -> opts
    end
  end

  @doc """
  Gets all of the ash context so it can be set into a new process.

  Use `transfer_context/1` in the new process to set the context.
  """
  @spec get_context_for_transfer(opts :: Keyword.t()) :: term
  def get_context_for_transfer(opts \\ []) do
    context = Ash.get_context()
    actor = Process.get(:ash_actor)
    authorize? = Process.get(:ash_authorize?)
    tenant = Process.get(:ash_tenant)
    tracer = Process.get(:ash_tracer)

    tracer_context =
      opts[:tracer]
      |> List.wrap()
      |> Enum.concat(List.wrap(tracer))
      |> Map.new(fn tracer ->
        {tracer, Ash.Tracer.get_span_context(tracer)}
      end)

    %{
      context: context,
      actor: actor,
      tenant: tenant,
      authorize?: authorize?,
      tracer: tracer,
      tracer_context: tracer_context
    }
  end

  @spec transfer_context(term, opts :: Keyword.t()) :: :ok
  def transfer_context(
        %{
          context: context,
          actor: actor,
          tenant: tenant,
          authorize?: authorize?,
          tracer: tracer,
          tracer_context: tracer_context
        },
        _opts \\ []
      ) do
    case actor do
      {:actor, actor} ->
        Ash.set_actor(actor)

      _ ->
        :ok
    end

    case tenant do
      {:tenant, tenant} ->
        Ash.set_tenant(tenant)

      _ ->
        :ok
    end

    case authorize? do
      {:authorize?, authorize?} ->
        Ash.set_authorize?(authorize?)

      _ ->
        :ok
    end

    Ash.set_tracer(tracer)

    Enum.each(tracer_context || %{}, fn {tracer, tracer_context} ->
      Ash.Tracer.set_span_context(tracer, tracer_context)
    end)

    Ash.set_context(context)
  end

  @doc """
  Sets context into the process dictionary that is used for all changesets and queries.

  In Ash 3.0, this will be updated to deep merge
  """
  @spec set_context(map) :: :ok
  def set_context(map) do
    Process.put(:ash_context, map)

    :ok
  end

  @doc """
  Deep merges context into the process dictionary that is used for all changesets and queries.
  """
  @spec merge_context(map) :: :ok
  def merge_context(map) do
    update_context(&Ash.Helpers.deep_merge_maps(&1, map))

    :ok
  end

  @doc """
  Updates the context into the process dictionary that is used for all changesets and queries.
  """
  @spec update_context((map -> map)) :: :ok
  def update_context(fun) do
    context = Process.get(:ash_context, %{})
    set_context(fun.(context))

    :ok
  end

  @doc """
  Sets actor into the process dictionary that is used for all changesets and queries.
  """
  @spec set_actor(map) :: :ok
  def set_actor(map) do
    Process.put(:ash_actor, {:actor, map})

    :ok
  end

  @doc """
  Sets authorize? into the process dictionary that is used for all changesets and queries.
  """
  @spec set_authorize?(map) :: :ok
  def set_authorize?(map) do
    Process.put(:ash_authorize?, {:authorize?, map})

    :ok
  end

  @doc """
  Sets the tracer into the process dictionary that will be used to trace requests
  """
  @spec set_tracer(module | list(module)) :: :ok
  def set_tracer(module) do
    case Process.get(:ash_tracer, module) do
      nil -> Process.put(:ash_tracer, module)
      tracer -> Process.put(:ash_tracer, Enum.uniq(List.wrap(tracer) ++ List.wrap(module)))
    end

    :ok
  end

  @doc """
  Removes a tracer from the process dictionary.
  """
  @spec remove_tracer(module | list(module)) :: :ok
  def remove_tracer(module) do
    case Process.get(:ash_tracer, module) do
      nil -> :ok
      tracer -> Process.put(:ash_tracer, List.wrap(tracer) -- List.wrap(module))
    end

    :ok
  end

  @doc """
  Gets the current actor from the process dictionary
  """
  @spec get_actor() :: term()
  def get_actor do
    case Process.get(:ash_actor) do
      {:actor, value} ->
        value

      _ ->
        nil
    end
  end

  @doc """
  Gets the current tracer
  """
  @spec get_tracer() :: term()
  def get_tracer do
    case Process.get(:ash_tracer) do
      {:tracer, value} ->
        value

      _ ->
        Application.get_env(:ash, :tracer)
    end
  end

  @doc """
  Gets the current authorize? from the process dictionary
  """
  @spec get_authorize?() :: term()
  def get_authorize? do
    case Process.get(:ash_authorize?) do
      {:authorize?, value} ->
        value

      _ ->
        nil
    end
  end

  @doc """
  Sets tenant into the process dictionary that is used for all changesets and queries.
  """
  @spec set_tenant(String.t()) :: :ok
  def set_tenant(tenant) do
    Process.put(:ash_tenant, {:tenant, tenant})

    :ok
  end

  @doc """
  Gets the current tenant from the process dictionary
  """
  @spec get_tenant() :: term()
  def get_tenant do
    case Process.get(:ash_tenant) do
      {:tenant, value} ->
        value

      _ ->
        nil
    end
  end

  @doc """
  Gets the current context from the process dictionary
  """
  @spec get_context() :: term()
  def get_context do
    Process.get(:ash_context, %{}) || %{}
  end
end