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 """
Delete a leaf or a subtree.
To delete a leaf node set the limit option to: 1, and in this particular case
all the nodes that reference the leaf will be assigned to the leaf's immediate ancestor
If limit is 0, then the leaf and its descendants will be deleted
"""
def delete(pid, leaf, opts \\ [limit: 1])
def delete(pid, leaf, opts) do
leaf? = Keyword.get(opts, :limit, 1) == 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_opts = [itself: true] ++ Keyword.take(opts, [:depth])
descendants = _descendants(leaf, descendants_opts, config)
subtree =
Enum.filter(paths, fn
[ancestor, descendant, _] ->
ancestor in descendants && descendant in descendants
end)
nodes =
Enum.reduce(subtree, %{}, fn [ancestor, descendant, _depth], 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, %CTE{paths: paths} = config) do
leaf_parent =
case _ancestors(leaf, [itself: false], config) do
[leaf_parent | _ancestors] -> leaf_parent
_ -> nil
end
paths =
paths
|> Enum.reduce([], fn
[_leaf, ^leaf, _], acc -> acc
[^leaf, descendant, _depth], acc -> [[leaf_parent, descendant, 1] | acc]
p, acc -> [p | acc]
end)
|> Enum.reverse()
{:reply, :ok, %{config | paths: paths}}
end
@doc false
def handle_call({:delete, leaf, _subtree, opts}, _from, %{paths: paths} = config) do
opts = Keyword.put(opts, :itself, true)
descendants = _descendants(leaf, opts, config) || []
paths =
Enum.filter(paths, fn [_ancestor, descendant, _] ->
descendant not in descendants
end)
{: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_paths, _} = descendants_collector(leaf, [itself: true], config)
descendants = Enum.map(descendants_paths, fn [_, descendant, _] -> descendant end)
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_paths, _} =
ancestors_collector(ancestor, [itself: true], %{config | paths: paths_without_leaf})
new_paths =
for [ancestor, _, super_tree_depth] <- [[leaf, leaf, -1] | new_ancestors_paths],
[_, descendant, subtree_depth] <- descendants_paths,
into: [] do
[ancestor, descendant, super_tree_depth + subtree_depth + 1]
end
|> Enum.reverse()
{:reply, :ok, %{config | paths: paths_without_leaf ++ new_paths}}
end
@doc false
def handle_call({:insert, leaf, ancestor, opts}, _from, config) do
case _insert(leaf, ancestor, opts, config) do
{:ok, new_paths, config} -> {:reply, {:ok, new_paths}, config}
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
descendants_collector(ancestor, opts, config)
|> depth(opts, config)
|> selected(opts, config)
end
@doc false
defp descendants_collector(ancestor, opts, config) do
mapper = fn paths -> Enum.map(paths, fn [_, descendant, _] -> descendant end) end
fn path, {acc_paths, _mapper, size} = acc, itself? ->
case path do
[^ancestor, ^ancestor, _] when not itself? ->
acc
[^ancestor, _descendant, _depth] = descendants ->
{[descendants | acc_paths], mapper, size + 1}
_ ->
acc
end
end
|> _find_leaves(opts, config)
end
@doc false
defp _ancestors(descendant, opts, config) do
ancestors_collector(descendant, opts, config)
|> depth(opts, config)
|> selected(opts, config)
end
@doc false
defp ancestors_collector(descendant, opts, config) do
mapper = fn paths -> Enum.map(paths, fn [ancestor, _, _] -> ancestor end) end
fn path, {acc_paths, _mapper, size} = acc, itself? ->
case path do
[^descendant, ^descendant, _] when not itself? ->
acc
[_ancestor, ^descendant, _depth] = ancestors ->
{[ancestors | acc_paths], mapper, 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
case Map.has_key?(nodes, ancestor) do
true ->
{leaf_new_ancestors, _} = ancestors_collector(ancestor, [itself: true], config)
new_paths =
leaf_new_ancestors
|> Enum.reduce([[leaf, leaf, 0]], fn [ancestor, _, depth], acc ->
[[ancestor, leaf, depth + 1] | acc]
end)
acc_paths = paths ++ new_paths
config = %{config | paths: acc_paths}
{:ok, new_paths, config}
_ ->
{:error, :no_ancestor, config}
end
end
@doc false
defp _find_leaves(fun, opts, %CTE{paths: paths}) do
itself? = Keyword.get(opts, :itself, false)
limit = Keyword.get(opts, :limit, 0)
{leaves_paths, mapper, _size} =
paths
|> Enum.reduce_while({[], & &1, 0}, fn path, acc ->
{_, _, sz} = dsz = fun.(path, acc, itself?)
if limit == 0 or sz < limit, do: {:cont, dsz}, else: {:halt, dsz}
end)
{leaves_paths, mapper}
end
@doc false
defp depth({leaves_paths, mapper}, opts, _config) do
leaves_paths =
if depth = Keyword.get(opts, :depth) do
leaves_paths
|> Enum.filter(fn [_, _, depth_] -> depth_ <= max(depth, 0) end)
else
leaves_paths
end
{leaves_paths, mapper}
end
@doc false
defp selected({leaves_paths, mapper}, opts, %CTE{nodes: nodes}) do
leaves = mapper.(leaves_paths)
if Keyword.get(opts, :nodes, false) do
Enum.map(leaves, &Map.get(nodes, &1))
else
leaves
end
end
end