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