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_opts = [itself: true] ++ Keyword.take(opts, [:depth])
descendants = _descendants(leaf, descendants_opts, config)
subtree =
paths
|> Enum.filter(fn [ancestor, descendant, _] ->
ancestor in descendants && descendant in descendants
end)
nodes =
subtree
|> Enum.reduce(%{}, 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, 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_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