defmodule Mix.Tasks.Frontier.Spec do @shortdoc "Prints the Frontier boundary spec for the project." @moduledoc """ Prints a human-readable summary of all declared Frontier boundaries. $ mix frontier.spec Shows each context with its exports, internals, reaches, and externals configuration. """ use Mix.Task alias Frontier.Classifier alias Frontier.Store @impl Mix.Task def run(_argv) do Mix.Task.run("compile") Frontier.Store.ensure_loaded() print_root() print_contexts() end defp print_root do case Store.root() do nil -> :ok {root_module, config} -> Mix.shell().info("#{inspect(root_module)} (root)") if config.globals != [] do globals = Enum.map_join(config.globals, ", ", &inspect/1) Mix.shell().info(" globals: #{globals}") end if config.ignore != [] do ignored = Enum.map_join(config.ignore, ", ", &inspect/1) Mix.shell().info(" ignore: #{ignored}") end Mix.shell().info("") end end defp print_contexts do contexts = Store.contexts() if contexts == %{} do Mix.shell().info("No contexts declared.") else contexts |> Enum.sort_by(fn {mod, _} -> inspect(mod) end) |> Enum.each(fn {module, config} -> Mix.shell().info(inspect(module)) print_exports(module, config) print_internals(module) print_reaches(config) print_externals(config) print_enforcement(config) print_skip_violations(config) Mix.shell().info("") end) end end defp print_exports(module, config) do schemas = find_schemas(module) manual_exports = if is_list(config.exports), do: config.exports, else: [] reclassified = Store.all_reclassified() |> Enum.filter(fn {_mod, target} -> target == module end) |> Enum.map(fn {mod, _} -> mod end) all_exports = Enum.map(schemas, &{&1, "schema"}) ++ Enum.map(manual_exports, &{&1, "manual"}) ++ Enum.map(reclassified, &{&1, "reclassified"}) case {config.exports, all_exports} do {:all, _} -> Mix.shell().info(" exports: :all") {_, []} -> :ok {_, exports} -> formatted = Enum.map_join(exports, ", ", fn {mod, type} -> "#{inspect(mod)} (#{type})" end) Mix.shell().info(" exports: #{formatted}") end end defp print_internals(module) do module_prefix = Atom.to_string(module) <> "." internals = :code.all_loaded() |> Enum.map(fn {mod, _} -> mod end) |> Enum.filter(fn mod -> String.starts_with?(Atom.to_string(mod), module_prefix) && Classifier.classify(mod) == :internal end) |> Enum.sort() if internals != [] do formatted = Enum.map_join(internals, ", ", &inspect/1) Mix.shell().info(" internals: #{formatted}") end end defp print_reaches(%{reaches: nil}), do: Mix.shell().info(" reaches: (unrestricted)") defp print_reaches(%{reaches: reaches}) do formatted = Enum.map_join(reaches, ", ", &inspect/1) Mix.shell().info(" reaches: #{formatted}") end defp print_externals(%{externals: nil}), do: Mix.shell().info(" externals: (unrestricted)") defp print_externals(%{externals: externals}) do formatted = Enum.map_join(externals, ", ", &inspect/1) Mix.shell().info(" externals: #{formatted}") end defp print_enforcement(%{enforce: %{callers: true, calls: true}}), do: :ok defp print_enforcement(%{enforce: enforce}) do flags = [] |> then(&if(enforce.callers, do: &1, else: ["callers disabled" | &1])) |> then(&if(enforce.calls, do: &1, else: ["calls disabled" | &1])) |> Enum.join(", ") Mix.shell().info(" enforcement: #{flags}") end defp print_skip_violations(%{skip_violations: []}), do: :ok defp print_skip_violations(%{skip_violations: skips}) do formatted = Enum.map_join(skips, ", ", &inspect/1) Mix.shell().info(" skip_violations: #{formatted}") end defp find_schemas(context_module) do prefix = Atom.to_string(context_module) <> "." :code.all_loaded() |> Enum.map(fn {mod, _} -> mod end) |> Enum.filter(fn mod -> String.starts_with?(Atom.to_string(mod), prefix) && Classifier.classify(mod) == :schema end) |> Enum.sort() end end