Packages

A resource declaration and interaction library. Built with pluggable data layers, and designed to be used by multiple front ends.

Retired package: Renamed - renamed to ash

Current section

Files

Jump to
ash_core lib ash data_layer ets.ex
Raw

lib/ash/data_layer/ets.ex

defmodule Ash.DataLayer.Ets do
@moduledoc """
An ETS (Erlang Term Storage) backed Ash Datalayer, for testing.
This is used for testing. *Do not use this data layer in production*
"""
alias Ash.Actions.Sort
alias Ash.Filter.{Expression, Not, Predicate}
alias Ash.Filter.Predicate.{Eq, GreaterThan, In, LessThan}
@behaviour Ash.DataLayer
@ets %Ash.Dsl.Section{
name: :ets,
describe: """
A section for configuring the ets data layer
""",
schema: [
private?: [
type: :boolean,
default: false
]
]
}
use Ash.Dsl.Extension, sections: [@ets]
alias Ash.Dsl.Extension
@spec private?(Ash.resource()) :: boolean
def private?(resource) do
Extension.get_opt(resource, [:ets], :private?, false, true)
end
defmodule Query do
@moduledoc false
defstruct [:resource, :filter, :limit, :sort, relationships: %{}, offset: 0]
end
@impl true
def can?(resource, :async_engine) do
not private?(resource)
end
def can?(_, :composite_primary_key), do: true
def can?(_, :upsert), do: true
def can?(_, :boolean_filter), do: true
def can?(_, :transact), do: false
def can?(_, :delete_with_query), do: false
def can?(_, {:filter_predicate, _, %In{}}), do: true
def can?(_, {:filter_predicate, _, %Eq{}}), do: true
def can?(_, {:filter_predicate, _, %LessThan{}}), do: true
def can?(_, {:filter_predicate, _, %GreaterThan{}}), do: true
def can?(_, {:sort, _}), do: true
def can?(_, _), do: false
@impl true
def resource_to_query(resource) do
%Query{
resource: resource
}
end
@impl true
def limit(query, offset, _), do: {:ok, %{query | limit: offset}}
@impl true
def offset(query, offset, _), do: {:ok, %{query | offset: offset}}
@impl true
def filter(query, filter, _resource) do
{:ok, %{query | filter: filter}}
end
@impl true
def sort(query, sort, _resource) do
{:ok, %{query | sort: sort}}
end
@impl true
def run_query(
%Query{resource: resource, filter: filter, offset: offset, limit: limit, sort: sort},
_resource
) do
with {:ok, records} <- get_records(resource),
filtered_records <- filter_matches(records, filter) do
offset_records =
filtered_records
|> Sort.runtime_sort(sort)
|> Enum.drop(offset || 0)
limited_records =
if limit do
Enum.take(offset_records, limit)
else
offset_records
end
{:ok, limited_records}
else
{:error, error} -> {:error, error}
end
end
defp get_records(resource) do
with {:ok, table} <- wrap_or_create_table(resource),
{:ok, record_tuples} <- ETS.Set.to_list(table) do
{:ok, Enum.map(record_tuples, &elem(&1, 1))}
end
end
def filter_matches(records, nil), do: records
def filter_matches(records, filter) do
Enum.filter(records, &matches_filter?(&1, filter.expression))
end
defp matches_filter?(_record, nil), do: true
defp matches_filter?(_record, boolean) when is_boolean(boolean), do: boolean
defp matches_filter?(
record,
%Predicate{
predicate: predicate,
attribute: %{name: name},
relationship_path: []
}
) do
matches_predicate?(record, name, predicate)
end
defp matches_filter?(record, %Expression{op: :and, left: left, right: right}) do
matches_filter?(record, left) && matches_filter?(record, right)
end
defp matches_filter?(record, %Expression{op: :or, left: left, right: right}) do
matches_filter?(record, left) || matches_filter?(record, right)
end
defp matches_filter?(record, %Not{expression: expression}) do
not matches_filter?(record, expression)
end
defp matches_predicate?(record, field, %Eq{value: predicate_value}) do
Map.fetch(record, field) == {:ok, predicate_value}
end
defp matches_predicate?(record, field, %LessThan{value: predicate_value}) do
case Map.fetch(record, field) do
{:ok, value} -> value < predicate_value
:error -> false
end
end
defp matches_predicate?(record, field, %GreaterThan{value: predicate_value}) do
case Map.fetch(record, field) do
{:ok, value} -> value > predicate_value
:error -> false
end
end
defp matches_predicate?(record, field, %In{values: predicate_values}) do
case Map.fetch(record, field) do
{:ok, value} -> value in predicate_values
:error -> false
end
end
@impl true
def upsert(resource, changeset) do
create(resource, changeset)
end
@impl true
def create(resource, changeset) do
pkey =
resource
|> Ash.Resource.primary_key()
|> Enum.into(%{}, fn attr ->
{attr, Ash.Changeset.get_attribute(changeset, attr)}
end)
with {:ok, table} <- wrap_or_create_table(resource),
record <- Ash.Changeset.apply_attributes(changeset),
{:ok, _} <- ETS.Set.put(table, {pkey, record}) do
{:ok, record}
else
{:error, error} -> {:error, error}
end
end
@impl true
def destroy(%resource{} = record) do
pkey = Map.take(record, Ash.Resource.primary_key(resource))
with {:ok, table} <- wrap_or_create_table(resource),
{:ok, _} <- ETS.Set.delete(table, pkey) do
:ok
else
{:error, error} -> {:error, error}
end
end
@impl true
def update(resource, changeset) do
create(resource, changeset)
end
defp wrap_or_create_table(resource) do
case ETS.Set.wrap_existing(resource) do
{:error, :table_not_found} ->
protection =
if private?(resource) do
:private
else
:public
end
ETS.Set.new(
name: resource,
protection: protection,
ordered: true,
read_concurrency: true
)
{:ok, table} ->
{:ok, table}
{:error, other} ->
{:error, other}
end
end
end