lib/dependents/tree/digraph.ex

defmodule Dependents.Tree.Digraph do
  @moduledoc """
  In a diagram of a graph, a vertex is usually represented by a circle with a
  label, and an edge is represented by an arrow extending from one vertex to
  another.
  
  Here vertices are apps and edges are arrows extending from an app to its
  dependents.
  """

  alias Dependents.Tree

  @typedoc "Erlang digraph"
  @type t :: :digraph.graph()

  @doc """
  Converts a `Dependents.Tree` into a digraph.
  """
  @spec from_tree(Tree.t()) :: t
  def from_tree(tree) do
    # A digraph is a mutable data structure.
    digraph = :digraph.new([:acyclic])

    for app <- Map.keys(tree) do
      :digraph.add_vertex(digraph, app)
    end

    for {app, [_dcys | deps]} <- tree, deps != [] do
      for dep <- deps do
        # The edge is emanating from `app` and incident on `dep`.
        :digraph.add_edge(digraph, app, dep)
      end
    end

    digraph
  end

  @doc """
  Assigns each app (vertex) to its topological rank in `digraph`.
  """
  @spec ranks(t) :: Tree.ranks()
  def ranks(digraph) do
    :digraph_utils.topsort(digraph)
    |> Enum.with_index(1)
    |> Map.new()
  end

  @doc """
  Returns a recursive list of the out-neighbors of `app`.
  
  If an edge is emanating from v and incident on w, then w is said to be an
  out-neighbor of v, and v is said to be an in-neighbor of w.
  """
  @spec dependents(t, Tree.app()) :: [Tree.dep()]
  def dependents(digraph, app) do
    dependents(digraph, [app], [])
  end

  ## Private functions

  @spec dependents(t, [Tree.app()], [Tree.dep()]) :: [Tree.dep()]
  defp dependents(_digraph, [], deps), do: Enum.uniq(deps)

  defp dependents(digraph, [app | apps], deps) do
    neighbors = :digraph.out_neighbours(digraph, app)
    dependents(digraph, apps ++ neighbors, deps ++ neighbors)
  end
end