lib/ash/api/global_interface.ex

defmodule Ash.Api.GlobalInterface do
  @moduledoc "The interface for calling any Ash api. Use `Ash` to call these functions."
  for {function, arity} <- Ash.Api.Functions.functions() do
    if function == :load do
      def load({:ok, result}, load) do
        load(result, load)
      end

      def load({:error, error}, _), do: {:error, error}

      def load([], _), do: {:ok, []}
      def load(nil, _), do: {:ok, nil}

      def load(%page_struct{results: []} = page, _)
          when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
        {:ok, page}
      end
    end

    if function == :load! do
      def load!({:ok, result}, load) do
        {:ok, load!(result, load)}
      end

      def load!({:error, error}, _), do: raise(Ash.Error.to_error_class(error))
      def load!([], _), do: []
      def load!(nil, _), do: nil

      def load!(%page_struct{results: []} = page, _)
          when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
        page
      end
    end

    args = Macro.generate_arguments(arity, __MODULE__)

    docs_arity =
      if function in Ash.Api.Functions.no_opts_functions() do
        arity
      else
        arity + 1
      end

    @doc "Calls `c:Ash.Api.#{function}/#{docs_arity}` on the resource's configured api. See those callback docs for more."
    def unquote(function)(unquote_splicing(args)) do
      resource = resource_from_args!(unquote(function), unquote(arity), [unquote_splicing(args)])

      api = Ash.Resource.Info.api(resource)

      if !api do
        raise_no_api_error!(resource, unquote(function), unquote(arity))
      end

      apply(api, unquote(function), [unquote_splicing(args)])
    end

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

      if function == :load! do
        def load!({:ok, result}, load, opts) do
          {:ok, load(result, load, opts)}
        end

        def load!({:error, error}, _, _), do: raise(Ash.Error.to_error_class(error))

        def load!(nil, _, _), do: nil
        def load!([], _, _), do: []

        def load!(%page_struct{results: []} = page, _, _)
            when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
          page
        end
      end

      if function == :load do
        def load({:ok, result}, load, opts) do
          load(result, load, opts)
        end

        def load({:error, error}, _, _), do: {:error, error}
        def load([], _, _), do: {:ok, []}
        def load(nil, _, _), do: {:ok, nil}

        def load(%page_struct{results: []} = page, _, _)
            when page_struct in [Ash.Page.Keyset, Ash.Page.Offset] do
          {:ok, page}
        end
      end

      @doc "Calls `c:Ash.Api.#{function}/#{arity + 1}` on the resource's configured api. See those callback docs for more."
      def unquote(function)(unquote_splicing(args)) do
        resource =
          resource_from_args!(unquote(function), unquote(arity), [unquote_splicing(args)])

        api = Ash.Resource.Info.api(resource)

        if !api do
          raise_no_api_error!(resource, unquote(function), unquote(arity))
        end

        apply(api, unquote(function), [unquote_splicing(args)])
      end
    end
  end

  defp raise_no_api_error!(resource, function, arity) do
    raise ArgumentError, """
    No api configured for resource #{inspect(resource)}.

    To configure one, use `api: MyApi` in the resource's options, for example: `use Ash.Resource, api: YourApi`.

    If the resource is meant to be used with multiple Apis (a rare case), call that api direction instead of using `Ash.#{function}/#{arity}`.
    """
  end

  defp resource_from_args!(fun, _, [data | _]) when fun in [:load, :load!] do
    case data do
      %struct{rerun: {%Ash.Query{resource: resource}, _}}
      when struct in [Ash.Page.Keyset, Ash.Page.Offset] ->
        resource

      %struct{rerun: {resource, _}}
      when struct in [Ash.Page.Keyset, Ash.Page.Offset] and is_atom(resource) ->
        resource

      %resource{} ->
        resource

      [%resource{} | _] ->
        resource

      {:ok, %resource{}} ->
        resource

      {:ok, [%resource{} | _]} ->
        resource
    end
  end

  defp resource_from_args!(:reload, _, [%resource{} | _]) do
    resource
  end

  defp resource_from_args!(fun, _, [_, resource | _]) when fun in [:bulk_create, :bulk_create!] do
    resource
  end

  defp resource_from_args!(fun, _, [resource | _])
       when fun in [:calculate, :calculate!, :get, :get!] do
    resource
  end

  defp resource_from_args!(fun, _, [page | _]) when fun in [:page, :page!] do
    case page do
      %struct{rerun: {%Ash.Query{resource: resource}, _}}
      when struct in [Ash.Page.Keyset, Ash.Page.Offset] ->
        resource

      %struct{rerun: {resource, _}}
      when struct in [Ash.Page.Keyset, Ash.Page.Offset] and is_atom(resource) ->
        resource

      other ->
        raise """
        Could not determine resource. Expected an `Ash.Page.Keyset` or `Ash.Page.Offset`.

        Got: #{inspect(other)}
        """
    end
  end

  defp resource_from_args!(fun, _, [query_or_changeset_or_action | _])
       when fun in [:can, :can?] do
    case query_or_changeset_or_action do
      %struct{resource: resource} when struct in [Ash.Changeset, Ash.Query, Ash.ActionInput] ->
        resource

      {resource, _} ->
        resource

      {resource, _, _} ->
        resource
    end
  end

  defp resource_from_args!(fun, _arity, [changeset_or_query | _])
       when fun in [
              :destroy,
              :update,
              :destroy!,
              :update!,
              :read,
              :read!,
              :stream,
              :stream!,
              :create,
              :create!,
              :run_action,
              :run_action!,
              :read_one,
              :read_one!,
              :get,
              :count,
              :count!,
              :first,
              :first!,
              :sum,
              :sum!,
              :min,
              :min!,
              :max,
              :max!,
              :avg,
              :avg!,
              :exists,
              :exists?,
              :list,
              :list!,
              :aggregate,
              :aggregate!
            ] do
    case changeset_or_query do
      %struct{resource: resource} when struct in [Ash.Changeset, Ash.Query, Ash.ActionInput] ->
        resource

      resource when is_atom(resource) and not is_nil(resource) ->
        resource

      other ->
        raise """
        Could not determine resource. Expected a changeset, query, action input, or resource.

        Got: #{inspect(other)}
        """
    end
  end

  defp resource_from_args!(fun, arity, _args) do
    raise ArgumentError, "Could not determine resource from arguments to `Ash.#{fun}/#{arity}`"
  end
end