defmodule CTE.Utils do @moduledoc """ Basic utilities for helping developing functionality about the `CTE` data structures """ @doc """ render a path hierarchy as a .dot string that you could use for drawing your results, using graphviz. w⦿‿⦿t! Upon receiving something like this: %{ nodes: %{ 6 => %{ author: "Rolie", comment: "Everything is easier, than with the Nested Sets.", id: 6 }, 8 => %{ author: "Olie", comment: "I’m sold! And I’ll use its Elixir implementation! <3", id: 8 }, 9 => %{author: "Polie", comment: "w⦿‿⦿t!", id: 9} }, paths: [ [6, 6, 0], [6, 8, 1], [8, 8, 0], [6, 9, 1], [9, 9, 0] ] } this will output a .dot formatted string that you could use later for generating an image: `dot -Tpng .dot -o .png` """ @spec print_dot(map, Keyword.t()) :: String.t() def print_dot(tree, opts \\ []) def print_dot(%{paths: paths, nodes: nodes}, opts) when is_list(paths) and is_map(nodes) do labels = Keyword.get(opts, :labels, []) [[root, _, _] | paths] = paths root = Map.get(nodes, root) acc = "digraph #{dot_bubble(root, labels)} {" Enum.reduce(paths, acc, fn [ancestor, descendant, _depth], acc -> parent = Map.get(nodes, ancestor) child = Map.get(nodes, descendant) acc <> "\n " <> build_dot(parent, child, labels) end) <> "\n}\n" end def print_dot(_, _), do: {:error, :invalid_argument} @doc """ print the tree at the console, using a custom function for selecting the info to be displayed The print_tree/3 function receives the tree structure returned by the CTE, the id of an existing node we want to start printing the tree with, followed by the options. ## Options * `:callback` - a function that it is invoked for every node in the tree. Has two parameters: * id - the id of the node we render, ao a node specific unique identifier i.e. node name, etc. * nodes - the nodes received the tree structure, a map %{node_id => node}...any() This function must return a tuple with two elements. First element is the name of the node we render, the second one being any optional info you want to add. * `:raw` - return a list of tuples if true. Each tuple will contain the depth of the text returned from the callback. Useful for custom formatting the output of the print. Example, using the default options: iex» {:ok, tree} = CTT.tree(1) iex» CTE.Utils.print_tree(tree,1, callback: &({&2[&1].author <> ":", &2[&1].comment})) Olie: Is Closure Table better than the Nested Sets? └── Rolie: It depends. Do you need referential integrity? └── Olie: Yeah. """ def print_tree(tree, id, opts \\ []) def print_tree(%{paths: paths, nodes: nodes}, id, opts) do user_callback = Keyword.get(opts, :callback, fn id, _nodes -> {id, "#{inspect(id)} => more..."} end) direct_children = direct_children(paths) callback = fn node_id when not is_nil(node_id) -> {name, info} = user_callback.(node_id, nodes) {{name, info}, Map.get(direct_children, node_id, [])} end _print_tree([id], callback, opts) end @doc """ converts the given tree into a nested map This function receives a tree structure returned by the CTE and the id of an existing node we want to start from. It returns a nested map representation of the tree. ## Options * `:callback` - a function of arity one that accepts the current node as input and outputs a formatted node. Defaults to `& &1`. Example: iex» {:ok, tree} = CTT.tree(1) iex» CTE.Utils.tree_to_map(tree, 6, callback: &Map.take(&1, [:text])) %{ "children" => [ %{ "children" => [], "id" => 8, "node" => %{text: "I’m sold! And I’ll use its Elixir implementation! <3"} }, %{"children" => [], "id" => 9, "node" => %{text: "w⦿‿⦿t!"}} ], "id" => 6, "node" => %{text: "Everything is easier, than with the Nested Sets."} } """ def tree_to_map(%{paths: paths, nodes: nodes}, id, opts \\ []) do case nodes do %{^id => _} -> :ok _ -> raise ArgumentError, "id `#{id}` is not in the provided tree" end callback = opts[:callback] || (& &1) direct_children = direct_children(paths) _tree_to_map(id, direct_children, nodes, callback) end defp direct_children(paths) do paths |> Enum.filter(fn [a, d, depth] -> a != d && depth == 1 end) |> Enum.group_by(fn [a, _, _] -> a end, fn [_, d, _] -> d end) |> Enum.reduce(%{}, fn {parent, children}, acc -> descendants = children || [] Map.put(acc, parent, Enum.uniq(descendants)) end) end defp _print_tree(nodes, callback, opts) do case print_tree(nodes, _depth = [], _seen = %{}, callback, opts, []) do {_seen, [] = out} -> out {_, out} -> Enum.reverse(out) end end # credits where credits due: # - adapted from a Mix.Utils similar method defp print_tree(nodes, depth, seen, callback, opts, out) do {nodes, seen} = Enum.flat_map_reduce(nodes, seen, fn node, seen -> {{name, info}, children} = callback.(node) if Map.has_key?(seen, name) do {[{name, info, []}], seen} else {[{name, info, children}], Map.put(seen, name, true)} end end) print_every_node(nodes, depth, seen, callback, opts, out) end defp print_every_node([], _depth, seen, _callback, _opts, out), do: {seen, out} defp print_every_node([{_name, info, children} | nodes], depth, seen, callback, opts, out) do raw? = Keyword.get(opts, :raw, false) info = if(info, do: info, else: "") out = if raw? do [{length(depth), info} | out] else # info = if(info, do: " #{info}", else: "") # IO.puts("#{depth(depth)}#{prefix(depth, nodes)}#{name}#{info}") IO.puts("#{depth(depth)}#{prefix(depth, nodes)}#{info}") out end {seen, out} = print_tree(children, [nodes != [] | depth], seen, callback, opts, out) print_every_node(nodes, depth, seen, callback, opts, out) end defp depth([]), do: "" defp depth(depth), do: Enum.reverse(depth) |> tl |> Enum.map(&entry(&1)) defp entry(true), do: "│ " defp entry(false), do: " " defp prefix([], _), do: "" defp prefix(_, []), do: "└── " defp prefix(_, _), do: "├── " @spec build_dot(String.t(), String.t(), list) :: String.t() defp build_dot(parent, child, []), do: "#{parent} -> #{child}" defp build_dot(parent, child, labels) do "#{dot_bubble(parent, labels)} -> #{dot_bubble(child, labels)}" end defp dot_bubble(node, labels) do bubble_text = labels |> Enum.map(fn l when is_function(l) -> l.(node) || l l -> Map.get(node, l, l) end) |> Enum.join("") "\"#{bubble_text}\"" end defp _tree_to_map(root_id, direct_children, nodes, callback) do root_node = nodes |> Map.get(root_id) |> callback.() child_ids = Map.get(direct_children, root_id) || [] child_nodes = Enum.map(child_ids, &_tree_to_map(&1, direct_children, nodes, callback)) %{"id" => root_id, "node" => root_node, "children" => child_nodes} end end