lib/ash/data_layer/simple/simple.ex

defmodule Ash.DataLayer.Simple do
  @moduledoc """
  A data layer that returns structs.

  This is the data layer that is used under the hood
  by embedded resources, and resources without data layers.
  """

  use Spark.Dsl.Extension, transformers: [], sections: []

  @doc false
  def can?(_, :create), do: true
  def can?(_, :update), do: true
  def can?(_, :destroy), do: true
  def can?(_, :sort), do: true
  def can?(_, :limit), do: true
  def can?(_, {:sort, _}), do: true
  def can?(_, :filter), do: true
  def can?(_, :boolean_filter), do: true
  def can?(_, :nested_expressions), do: true
  def can?(_, {:filter_expr, _}), do: true
  def can?(_, :multitenancy), do: true
  def can?(_, _), do: false

  defmodule Query do
    @moduledoc false
    defstruct [:data, :resource, :filter, :api, :limit, sort: [], data_set?: false]
  end

  @doc """
  Sets the data for a query against a data-layer-less resource
  """
  def set_data(query, data) do
    query = Ash.Query.to_query(query)
    Ash.Query.set_context(query, %{data_layer: %{data: %{query.resource => data}}})
  end

  @doc false
  def resource_to_query(resource, api) do
    %Query{data: [], resource: resource, api: api}
  end

  @doc false
  def run_query(%{data_set?: false}, resource) do
    {:error,
     Ash.Error.SimpleDataLayer.NoDataProvided.exception(
       message: """
       No data provided to resource #{resource}. Perhaps you are missing a call to `Ash.DataLayer.Simple.set_data/2`?.

       Another common cause of this is failing to add a data layer for a resource. You can add a data layer like so:

       `use Ash.Resource, data_layer: Ash.DataLayer.Ets`
       """
     )}
  end

  def run_query(%{data: data, sort: sort, api: api, filter: filter, limit: limit}, _resource) do
    data
    |> do_filter_matches(filter, api)
    |> case do
      {:ok, results} ->
        {:ok,
         results
         |> Ash.Actions.Sort.runtime_sort(sort, api: api)
         |> then(fn data ->
           if limit do
             Enum.take(data, limit)
           else
             data
           end
         end)}

      {:error, error} ->
        {:error, error}
    end
  end

  defp do_filter_matches(data, filter, api) do
    Ash.Filter.Runtime.filter_matches(api, data, filter)
  end

  @doc false
  def limit(query, limit, _) do
    {:ok, %{query | limit: limit}}
  end

  @doc false
  def set_tenant(_, query, _), do: {:ok, query}

  @doc false
  def filter(query, filter, _resource) do
    {:ok, %{query | filter: filter}}
  end

  @doc false
  def sort(query, sort, _resource) do
    {:ok, %{query | sort: sort}}
  end

  @doc false
  def set_context(_resource, query, context) do
    with {:ok, data_layer_context} <- Map.fetch(context, :data_layer),
         {:ok, data} <- Map.fetch(data_layer_context, :data),
         {:ok, resource_data} <- Map.fetch(data, query.resource) do
      {:ok, %{query | data_set?: true, data: resource_data || []}}
    else
      _ ->
        {:ok, query}
    end
  end

  @doc false
  def create(_resource, changeset) do
    Ash.Changeset.apply_attributes(changeset)
  end

  @doc false
  def update(_resource, changeset) do
    Ash.Changeset.apply_attributes(changeset)
  end

  @doc false
  def destroy(_resource, _changeset) do
    :ok
  end
end