Choreo.Dependency (Choreo v0.7.1)

Copy Markdown View Source

Software dependency graph builder on top of Yog.

Choreo.Dependency models component relationships — modules, libraries, applications, interfaces, and tests — to help visualize and analyze coupling, layering, and circular dependencies.

When to use

Use Choreo.Dependency when refactoring a codebase, onboarding new developers, or enforcing architectural boundaries. It surfaces hidden cycles, measures instability, and identifies the deepest dependency chains that slow down builds and tests.

Node types

  • :application — deployable service or app
  • :library — external or shared library
  • :module — internal code module
  • :interface — API, contract, or protocol definition
  • :test — test suite or spec

Edge types

  • :uses — general dependency
  • :imports — explicit import / require
  • :calls — runtime function call
  • :inherits — inheritance / implementation
  • :dev — development-only dependency

Further reading

Quick Start

deps =
  Choreo.Dependency.new()
  |> Choreo.Dependency.add_application(:api, label: "API Gateway")
  |> Choreo.Dependency.add_library(:phoenix, label: "Phoenix")
  |> Choreo.Dependency.add_module(:auth, label: "Auth Module")
  |> Choreo.Dependency.depends_on(:api, :phoenix, type: :uses)
  |> Choreo.Dependency.depends_on(:api, :auth, type: :calls)

dot = Choreo.Dependency.to_dot(deps)

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.5, ranksep=1.0]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=9, penwidth=1.0]; auth [label="Auth Module", fillcolor="#10b981", shape="box"]; api [label="API Gateway", fillcolor="#3b82f6", shape="box3d"]; phoenix [label="Phoenix", fillcolor="#f59e0b", shape="cylinder"]; api -> auth [style="dotted", label="calls"]; api -> phoenix [label="uses"]; }

Analysis

# Find circular dependencies
Choreo.Dependency.Analysis.cyclic_dependencies(deps)
#=> [[:repo, :service, :repo]]

# Impact analysis: what breaks if :auth changes?
Choreo.Dependency.Analysis.affected_by(deps, :auth)
#=> [:api, :web]

# Check layer violations
layers = %{api: 3, web: 2, repo: 1}
Choreo.Dependency.Analysis.layer_violations(deps, layers)
#=> [{:repo, :api, "repo (layer 1) calls api (layer 3)"}]

Summary

Functions

Adds an application node (deployable service or app).

Defines a cluster for grouping nodes visually (e.g., by team or layer).

Adds an interface node (API, contract, protocol).

Adds a library node (external or shared dependency).

Adds a module node (internal code unit).

Adds a test node (test suite or spec).

Creates a dependency edge from one component to another.

Returns all dependency edges as {from, to, weight} tuples.

Returns all edges with their metadata as {from, to, weight, meta} tuples.

Creates a new empty dependency graph.

Returns all node IDs in the dependency graph.

Returns all nodes of a given type.

Renders the dependency graph to DOT format.

Returns the raw Yog.Graph struct underpinning the dependency graph.

Collapses parallel edges into a simple Graph for algorithm analysis.

Types

t()

@type t() :: %Choreo.Dependency{
  clusters: %{required(String.t()) => map()},
  edge_meta: %{optional(Yog.Multi.Graph.edge_id()) => map()},
  graph: Yog.Multi.Graph.t()
}

Functions

add_application(deps, id, opts \\ [])

Adds an application node (deployable service or app).

Options

Examples

iex> deps = Choreo.Dependency.new()
iex> deps = Choreo.Dependency.add_application(deps, :api, label: "API")
iex> Choreo.Dependency.nodes(deps)
[:api]
iex> Map.get(deps.graph.nodes, :api).node_type
:application
iex> Map.get(deps.graph.nodes, :api).label
"API"

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.5, ranksep=1.0]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=9, penwidth=1.0]; api [label="API Gateway", fillcolor="#3b82f6", shape="box3d"]; }

add_cluster(deps, name, opts \\ [])

@spec add_cluster(t(), String.t(), keyword()) :: t()

Defines a cluster for grouping nodes visually (e.g., by team or layer).

Options

Examples

iex> deps = Choreo.Dependency.new()
iex> deps = Choreo.Dependency.add_cluster(deps, "core", label: "Core")
iex> deps.clusters["cluster_core"].label
"Core"

add_interface(deps, id, opts \\ [])

Adds an interface node (API, contract, protocol).

Options

Examples

iex> deps = Choreo.Dependency.new()
iex> deps = Choreo.Dependency.add_interface(deps, :contract)
iex> Choreo.Dependency.nodes(deps)
[:contract]
iex> Map.get(deps.graph.nodes, :contract).node_type
:interface

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.5, ranksep=1.0]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=9, penwidth=1.0]; contract [label="contract", fillcolor="#8b5cf6", shape="diamond"]; }

add_library(deps, id, opts \\ [])

Adds a library node (external or shared dependency).

Options

Examples

iex> deps = Choreo.Dependency.new()
iex> deps = Choreo.Dependency.add_library(deps, :phx, label: "Phoenix")
iex> Choreo.Dependency.nodes(deps)
[:phx]
iex> Map.get(deps.graph.nodes, :phx).node_type
:library

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.5, ranksep=1.0]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=9, penwidth=1.0]; phx [label="Phoenix", fillcolor="#f59e0b", shape="cylinder"]; }

add_module(deps, id, opts \\ [])

Adds a module node (internal code unit).

Options

Examples

iex> deps = Choreo.Dependency.new()
iex> deps = Choreo.Dependency.add_module(deps, :auth)
iex> Choreo.Dependency.nodes(deps)
[:auth]
iex> Map.get(deps.graph.nodes, :auth).node_type
:module

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.5, ranksep=1.0]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=9, penwidth=1.0]; auth [label="auth", fillcolor="#10b981", shape="box"]; }

add_test(deps, id, opts \\ [])

Adds a test node (test suite or spec).

Options

Examples

iex> deps = Choreo.Dependency.new()
iex> deps = Choreo.Dependency.add_test(deps, :auth_test)
iex> Choreo.Dependency.nodes(deps)
[:auth_test]
iex> Map.get(deps.graph.nodes, :auth_test).node_type
:test

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.5, ranksep=1.0]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=9, penwidth=1.0]; auth_test [label="auth_test", fillcolor="#64748b", shape="note"]; }

depends_on(deps, from, to, opts \\ [])

Creates a dependency edge from one component to another.

Direction reads as "from depends on to".

Limitation

At most one edge is allowed per (from, to) pair. Adding a second dependency between the same components raises ArgumentError. Multigraph support (parallel edges) is planned for a future release.

Options

Examples

iex> deps = Choreo.Dependency.new()
iex> deps = deps
...>   |> Choreo.Dependency.add_application(:api)
...>   |> Choreo.Dependency.add_module(:auth)
...>   |> Choreo.Dependency.depends_on(:api, :auth)
iex> [{_, _, _, meta}] = Choreo.Dependency.edges_with_meta(deps)
iex> meta.type
:uses

Diagram

digraph G { graph [rankdir=TB, splines=spline, nodesep=0.5, ranksep=1.0]; node [shape=box, style=filled, fillcolor="white", fontname="Helvetica", fontsize=12, fontcolor="white"]; edge [arrowhead=normal, color="#64748b", style=solid, fontname="Helvetica", fontsize=9, penwidth=1.0]; auth [label="auth", fillcolor="#10b981", shape="box"]; api [label="api", fillcolor="#3b82f6", shape="box3d"]; api -> auth [label="uses"]; }
iex> deps = Choreo.Dependency.new()
iex> deps = deps
...>   |> Choreo.Dependency.add_application(:api)
...>   |> Choreo.Dependency.add_module(:auth)
...>   |> Choreo.Dependency.depends_on(:api, :auth, type: :calls)
iex> [{_, _, _, meta}] = Choreo.Dependency.edges_with_meta(deps)
iex> meta.type
:calls
iex> meta.label
"calls"

edges(dependency)

@spec edges(t()) :: [{Yog.node_id(), Yog.node_id(), any()}]

Returns all dependency edges as {from, to, weight} tuples.

Examples

iex> deps = Choreo.Dependency.new()
iex> deps = deps
...>   |> Choreo.Dependency.add_application(:api)
...>   |> Choreo.Dependency.add_module(:auth)
...>   |> Choreo.Dependency.depends_on(:api, :auth)
iex> Choreo.Dependency.edges(deps)
[{:api, :auth, 1}]

edges_with_meta(dependency)

@spec edges_with_meta(t()) :: [{Yog.node_id(), Yog.node_id(), any(), map()}]

Returns all edges with their metadata as {from, to, weight, meta} tuples.

new()

@spec new() :: t()

Creates a new empty dependency graph.

Dependency graphs are always directed.

Examples

iex> deps = Choreo.Dependency.new()
iex> Choreo.Dependency.nodes(deps)
[]
iex> Choreo.Dependency.edges(deps)
[]

nodes(dependency)

@spec nodes(t()) :: [Yog.node_id()]

Returns all node IDs in the dependency graph.

Examples

iex> deps = Choreo.Dependency.new()
iex> deps = deps
...>   |> Choreo.Dependency.add_application(:api)
...>   |> Choreo.Dependency.add_module(:auth)
iex> Enum.sort(Choreo.Dependency.nodes(deps))
[:api, :auth]

nodes_of_type(dependency, type)

@spec nodes_of_type(t(), atom()) :: [Yog.node_id()]

Returns all nodes of a given type.

Examples

iex> deps = Choreo.Dependency.new()
iex> deps = deps
...>   |> Choreo.Dependency.add_application(:a)
...>   |> Choreo.Dependency.add_application(:b)
...>   |> Choreo.Dependency.add_library(:c)
iex> Enum.sort(Choreo.Dependency.nodes_of_type(deps, :application))
[:a, :b]
iex> Choreo.Dependency.nodes_of_type(deps, :library)
[:c]
iex> Choreo.Dependency.nodes_of_type(deps, :module)
[]

theme(name \\ :default, overrides \\ [])

@spec theme(
  atom(),
  keyword()
) :: Choreo.Theme.t()

Returns a theme for Choreo.Dependency.

Examples

iex> theme = Choreo.Dependency.theme(:default, graph_rankdir: :lr)
iex> theme.graph_rankdir
:lr

to_dot(deps, opts \\ [])

@spec to_dot(
  t(),
  keyword()
) :: String.t()

Renders the dependency graph to DOT format.

Options

Examples

iex> deps = Choreo.Dependency.new()
iex> deps = deps
...>   |> Choreo.Dependency.add_application(:api, label: "API")
...>   |> Choreo.Dependency.add_module(:auth, label: "Auth")
...>   |> Choreo.Dependency.depends_on(:api, :auth)
iex> dot = Choreo.Dependency.to_dot(deps)
iex> String.contains?(dot, "digraph")
true
iex> String.contains?(dot, "API")
true
iex> String.contains?(dot, "Auth")
true

to_graph(dependency)

@spec to_graph(t()) :: Yog.graph()

Returns the raw Yog.Graph struct underpinning the dependency graph.

Examples

iex> deps = Choreo.Dependency.new()
iex> graph = Choreo.Dependency.to_graph(deps)
iex> graph.kind
:directed

to_simple_graph(dependency, opts \\ [])

@spec to_simple_graph(
  t(),
  keyword()
) :: Yog.Graph.t()

Collapses parallel edges into a simple Graph for algorithm analysis.