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, use `depend_on_resources/1` """ @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 """ Gets the resources of an Api module. Can be used at compile time. Liberal use of this can greatly increase compile times, or even cause compiler deadlocks. Use with care. """ @spec depend_on_resources(Macro.t()) :: Macro.t() defmacro depend_on_resources(api) do quote do if registry = Ash.Api.Info.registry(unquote(api)) do @external_resource List.to_string(registry.module_info(:compile)[:source]) for entry <- Ash.Registry.Info.entries(registry) do @external_resource List.to_string(entry.module_info(:compile)[:source]) entry end else [] end 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 | binary()) :: 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