lib/ash/api/info.ex

defmodule Ash.Api.Info do
  @moduledoc "Introspection tools for Ash.Api"

  alias Ash.Error.Invalid.NoSuchResource

  alias Spark.Dsl.Extension

  @doc """
  Gets the resources of an Api module. DO NOT USE AT COMPILE TIME.

  If you need the resource list at compile time, you will need to introduce a compile time
  dependency on all of the resources, and therefore should use the registry directly. `Registry |> Ash.Registry.Info.entries()`.
  """
  @spec resources(Ash.Api.t()) :: list(Ash.Resource.t())
  def resources(api) do
    if registry = registry(api) do
      Ash.Registry.Info.entries(registry)
    else
      []
    end
  end

  @doc "The resource registry for an api"
  @spec registry(Ash.Api.t()) :: atom | nil
  def registry(api) do
    Extension.get_opt(api, [:resources], :registry, nil, true)
  end

  @doc "The allow MFA for an api"
  @spec allow(Ash.Api.t()) :: mfa() | nil
  def allow(api) do
    Extension.get_opt(api, [:resources], :allow, nil, true)
  end

  @doc "The execution timeout for an api"
  @spec timeout(Ash.Api.t()) :: nil | :infinity | integer()
  def timeout(api) do
    Extension.get_opt(api, [:execution], :timeout, 30_000, true)
  end

  @doc "The short name for an api"
  @spec short_name(Ash.Api.t()) :: nil | :infinity | integer()
  def short_name(api) do
    Extension.get_opt(api, [:execution], :short_name, nil) || api.default_short_name()
  end

  @doc "The trace name for an api"
  @spec trace_name(Ash.Api.t()) :: String.t()
  def trace_name(api) do
    Extension.get_opt(api, [:execution], :trace_name, nil) || to_string(short_name(api))
  end

  @doc "The span_name for an api and resource combination"
  @spec span_name(Ash.Api.t(), Ash.Resource.t(), action :: atom) :: String.t()
  def span_name(api, resource, action) do
    "#{trace_name(api)}:#{Ash.Resource.Info.trace_name(resource)}.#{action}"
  end

  @doc "Names a telemetry event for a given api/resource combo"
  @spec telemetry_event_name(Ash.Api.t(), atom | list(atom)) :: list(atom)
  def telemetry_event_name(api, name) do
    List.flatten([:ash, short_name(api), name])
  end

  @doc "Whether or not the actor is always required for an api"
  @spec require_actor?(Ash.Api.t()) :: boolean
  def require_actor?(api) do
    Extension.get_opt(api, [:authorization], :require_actor?, false, true)
  end

  @doc "When authorization should happen for a given api"
  @spec authorize(Ash.Api.t()) :: :when_requested | :always | :by_default
  def authorize(api) do
    Extension.get_opt(api, [:authorization], :authorize, :when_requested, true)
  end

  @doc "Whether or not the api allows unregistered resources to be used with it"
  @spec allow_unregistered?(Ash.Api.t()) :: atom | nil
  def allow_unregistered?(api) do
    Extension.get_opt(api, [:resources], :allow_unregistered?, nil)
  end

  @doc """
  Returns `{:ok, resource}` if the resource can be used by the api, or `{:error, error}`.
  """
  @spec resource(Ash.Api.t(), Ash.Resource.t()) ::
          {:ok, Ash.Resource.t()} | {:error, Ash.Error.t()}
  def resource(api, resource) do
    cond do
      allow_unregistered?(api) ->
        if Spark.Dsl.is?(resource, Ash.Resource) do
          resource
        else
          nil
        end

      Ash.Resource.Info.embedded?(resource) ->
        resource

      true ->
        api
        |> resources()
        |> Enum.find(&(&1 == resource))
    end
    |> case do
      nil ->
        if allowed?(allow(api), resource) do
          {:ok, resource}
        else
          {:error, NoSuchResource.exception(resource: resource)}
        end

      resource ->
        {:ok, resource}
    end
  end

  @spec allowed?(mfa | nil, module()) :: boolean
  defp allowed?({m, f, a}, resource) do
    apply(m, f, List.wrap(a) ++ [resource])
  end

  defp allowed?(_, _), do: false
end