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