lib/runbox/utils/topology_sort.ex

defmodule Runbox.Utils.TopologySort do
  @moduledoc """
  Provides topology sorting capabilities.
  """

  @doc """
  Sort the given network by its topology.

  The network is given in the form of a list with elements `{node, subs}` where subs is a list of
  nodes the node is connected to. The function returns list of nodes sorted by the topology.

  The sort is stable - original ordering of the elements is maintained where possible.
  """
  @spec sort(network :: [{node, [id]}], id_fun :: (node -> id)) ::
          {:ok, sorted_network :: [node]} | {:error, :loop}
        when node: var, id: var
  def sort(network, id_fun \\ & &1) do
    network = Enum.map(network, fn {node, subs} -> {id_fun.(node), node, subs} end)
    do_sort(network, [])
  end

  # based on Kahn's algorithm
  defp do_sort([], result) do
    {:ok, Enum.reverse(result)}
  end

  defp do_sort(network, result) do
    # we must do this one by one to maintain the stability of the given order
    index = Enum.find_index(network, fn {_, _, subs} -> subs == [] end)

    if index do
      {{node_id, node, []}, network} = List.pop_at(network, index)
      network = Enum.map(network, fn {id, comp, subs} -> {id, comp, subs -- [node_id]} end)
      do_sort(network, [node | result])
    else
      {:error, :loop}
    end
  end
end