Current section
Files
Jump to
Current section
Files
lib/cte/adapter/memory.ex
defmodule CTE.Adapter.Memory do
@moduledoc """
Basic implementation of the CTE, using the memory for persisting the models. Adapter provided as a convenient way of using CTE in tests or during the development
"""
use CTE.Adapter
@doc false
def descendants(pid, ancestor, opts) do
GenServer.call(pid, {:descendants, ancestor, opts})
end
@doc false
def ancestors(pid, descendant, opts) do
GenServer.call(pid, {:ancestors, descendant, opts})
end
@doc false
def insert(pid, leaf, ancestor, opts) do
GenServer.call(pid, {:insert, leaf, ancestor, opts})
end
@doc false
def delete(pid, leaf, opts) do
leaf? = Keyword.get(opts, :limit, 0) == 1
GenServer.call(pid, {:delete, leaf, leaf?, opts})
end
@doc false
def move(pid, leaf, ancestor, opts) do
GenServer.call(pid, {:move, leaf, ancestor, opts})
end
@doc false
def tree(pid, leaf, opts) do
GenServer.call(pid, {:tree, leaf, opts})
end
@doc false
def handle_call({:tree, leaf, _opts}, _from, config) do
%CTE{paths: paths, nodes: nodes} = config
descendants = _descendants(leaf, [itself: true], config)
subtree = Enum.filter(paths, fn [ancestor, _descendant] -> ancestor in descendants end)
nodes =
subtree
|> Enum.reduce(%{}, fn [ancestor, descendant], acc ->
Map.merge(acc, %{
ancestor => Map.get(nodes, ancestor),
descendant => Map.get(nodes, descendant)
})
end)
{:reply, {:ok, %{paths: subtree, nodes: nodes}}, config}
end
@doc false
def handle_call({:delete, leaf, true, _opts}, _from, config) do
%CTE{paths: paths} = config
paths = Enum.filter(paths, fn [_ancestor, descendant] -> descendant != leaf end)
{:reply, :ok, %{config | paths: paths}}
end
@doc false
def handle_call({:delete, leaf, _subtree, opts}, _from, config) do
opts = Keyword.put(opts, :itself, true)
descendants = _descendants(leaf, opts, config)
paths = Enum.filter(descendants, &(&1 != leaf))
{:reply, :ok, %{config | paths: paths}}
end
@doc false
def handle_call({:move, leaf, ancestor, _opts}, _from, config) do
%CTE{paths: paths} = config
ex_ancestors = _ancestors(leaf, [itself: true], config)
descendants = _descendants(leaf, [itself: true], config)
paths_with_leaf =
paths
|> Enum.filter(fn [ancestor, descendant] ->
ancestor in ex_ancestors and descendant in descendants and ancestor != descendant
end)
paths_without_leaf = Enum.filter(paths, &(&1 not in paths_with_leaf))
new_ancestors =
_ancestors(ancestor, [itself: true], %{config | paths: paths_without_leaf})
|> Enum.reverse()
new_paths =
for ancestor <- new_ancestors ++ [leaf], descendant <- descendants, into: [] do
[ancestor, descendant]
end
{:reply, :ok, %{config | paths: paths_without_leaf ++ new_paths}}
end
@doc false
def handle_call({:insert, leaf, ancestor, opts}, _from, config) do
with {:ok, new_paths, config} <- _insert(leaf, ancestor, opts, config) do
{:reply, {:ok, new_paths}, config}
else
err -> {:reply, {:error, err}, config}
end
end
@doc false
def handle_call({:ancestors, descendant, opts}, _from, config) do
result =
_ancestors(descendant, opts, config)
|> Enum.reverse()
{:reply, {:ok, result}, config}
end
@doc false
def handle_call({:descendants, ancestor, opts}, _from, config) do
result =
_descendants(ancestor, opts, config)
|> Enum.reverse()
{:reply, {:ok, result}, config}
end
@doc false
defp _descendants(ancestor, opts, config) do
fn path, {descendants, size} = acc, itself? ->
case path do
[^ancestor, ^ancestor] when not itself? ->
acc
[^ancestor, descendant] ->
{[descendant | descendants], size + 1}
_ ->
acc
end
end
|> _find_leaves(opts, config)
end
@doc false
defp _ancestors(descendant, opts, config) do
fn path, {ancestors, size} = acc, itself? ->
case path do
[^descendant, ^descendant] when not itself? ->
acc
[ancestor, ^descendant] ->
{[ancestor | ancestors], size + 1}
_ ->
acc
end
end
|> _find_leaves(opts, config)
end
@doc false
defp _insert(leaf, ancestor, opts, config) do
%CTE{nodes: nodes, paths: paths} = config
with true <- Map.has_key?(nodes, ancestor) do
leaf_new_ancestors =
_ancestors(ancestor, opts, config)
|> Enum.map(&[&1, leaf])
new_paths =
[[leaf, leaf], [ancestor, leaf] | leaf_new_ancestors]
|> Enum.reverse()
config = %{config | paths: new_paths ++ paths}
{:ok, new_paths, config}
else
_ -> {:error, :no_ancestor, config}
end
end
@doc false
defp _find_leaves(fun, opts, %CTE{nodes: nodes, paths: paths}) do
nodes? = Keyword.get(opts, :nodes, false)
itself? = Keyword.get(opts, :itself, false)
limit = Keyword.get(opts, :limit, 0)
{leaves, _size} =
paths
|> Enum.reduce_while({[], 0}, fn path, acc ->
{_, sz} = dsz = fun.(path, acc, itself?)
if limit == 0 or sz < limit, do: {:cont, dsz}, else: {:halt, dsz}
end)
if nodes?, do: Enum.map(leaves, &Map.get(nodes, &1)), else: leaves
end
end