Powered by AppSignal & Oban Pro

Choreo UML: Comprehensive Walkthrough

livebooks/guides/uml_walkthrough.livemd

Choreo UML: Comprehensive Walkthrough

# For Hex publication/readers:
Mix.install([
  {:choreo, "~> 0.14.1"},
  # {:choreo, path: Path.expand("../..", __DIR__), force: true},
  {:kino_vizjs, "~> 0.9.0"}
])

# For local development:
# Mix.install([
#   {:choreo, path: Path.expand("~/repos/elixir/choreo")},
#   {:kino_vizjs, "~> 0.9.0"}
# ])

Introduction

Choreo.UML is a software component and class modeler designed to represent UML Class Diagrams natively in Elixir. Even though Elixir is a functional language, large systems are heavily composed of structural blocks like Structs, Behaviors, and Protocols.

Choreo.UML lets you model these components directly in code, declare exact fields/attributes (with optional types and visibilities), define functions/operations, specify standard structural relationships, and visually render them beautifully to Graphviz/DOT (using 3-compartment HTML record tables) or Mermaid.js (supporting both flowchart layouts and native classDiagram blocks).

Choreo provides two ways to work with UML diagrams:

  1. Programmatic Pipe API (Choreo.UML) — A stable, pipe-first interface ideal for dynamic builders, code generation, and automated analysis.
  2. Lab DSL (Choreo.Lab.DSL.UML) — A concise, Livebook-friendly syntax for sketching class architectures and exploring type hierarchies.

The introductory example below uses the explicit pipe-first syntax. All subsequent examples throughout this guide demonstrate the Lab DSL.

alias Choreo.UML
alias Choreo.UML.Analysis
import Choreo.Lab.DSL.UML

# Initialize a standard user and authentication architecture using pipe syntax
uml =
  UML.new()
  # Add a struct representing a user database entity
  |> UML.add_class(:user,
    type: :struct,
    label: "User Struct",
    fields: [
      %{name: :id, type: :integer, visibility: :public},
      %{name: :email, type: :string, visibility: :private},
      %{name: :hashed_password, type: :string, visibility: :private}
    ],
    functions: [
      %{name: "authenticate", arity: 2, return: :boolean, visibility: :public}
    ]
  )
  # Add a behavior representing an authentication provider contract
  |> UML.add_class(:auth_provider,
    type: :behavior,
    label: "AuthProvider Contract",
    functions: [
      %{name: "verify", arity: 1, return: :ok_error, visibility: :public}
    ]
  )
  # Add an interface representing a generic caching store
  |> UML.add_class(:cache,
    type: :interface,
    functions: [
      %{name: "get", arity: 1, return: :any},
      %{name: "put", arity: 2, return: :ok}
    ]
  )
  # Model relationship semantics
  |> UML.add_relationship(:user, :auth_provider, type: :realizes, label: "implements")
  |> UML.add_relationship(:auth_provider, :cache, type: :depends, label: "uses")

Kino.Layout.tabs(
  Siren: Choreo.Lab.Siren.new(UML.to_mermaid(uml, syntax: :class_diagram)),
  Graphviz: Kino.VizJS.render(UML.to_dot(uml), height: "500px"),
  Sketch: Choreo.Lab.Sketch.new(UML.to_mermaid(uml, syntax: :class_diagram))
)

Component Modeling & Visibility

Every class, struct, behavior, or interface is rigorously validated via NimbleOptions to ensure structural sanity.

Fields and Functions Validation

  • Fields: Supports :name (Required atom or string), :type (Optional), and :visibility (Optional: :public (+), :private (-), :protected (#), or nil).
  • Functions: Supports :name (Required atom or string), :arity (Optional), :return (Optional), and :visibility (Optional).

Note on names: Both :name and "name" are accepted, but they are stored as strings internally. Comparisons (e.g., in broken_contracts/1) use strict equality, so be consistent.

# Validation will trigger a clean schema error if fields are invalid
try do
  uml do
    class "bad_module" do
      field :id, :integer, visibility: :invalid_visibility
    end
  end
rescue
  e -> e
end

Relationship Types

Relationships between software blocks map exactly to standard UML connectors:

Relationship Type Arrow Style Connector Style Intended Semantics
:inherits Hollow Solid (`-- >`)
:realizes Hollow Dashed (`.. >`)
:associates Solid Solid (-->) Composition (e.g. struct nesting/embedding).
:depends Open Dashed (..>) Run-time dependency or function calling invocation.
# Quick example of behavior inheritance using Lab DSL
inheritance_uml =
  uml do
    base = class("BaseHandler")
    http = class("HttpHandler")

    inherits http ~> base, "inherits_from"
  end

Kino.Layout.tabs(
  Siren: Choreo.Lab.Siren.new(UML.to_mermaid(inheritance_uml, syntax: :class_diagram)),
  Graphviz: Kino.VizJS.render(UML.to_dot(inheritance_uml))
)

Strict Contract Validation

By default, Choreo validates behavior contracts during static analysis via Analysis.broken_contracts/1. However, if you want to actively prevent invalid relationships from being added during schema construction, you can enable the :strict_contract_validation flag upon initialization:

# This will fail because strict_contract_validation is active and provider is missing the verify/1 function
try do
  uml strict_contract_validation: true do
    auth =
      behavior("AuthProvider") do
        function :verify, 1
      end

    provider = struct("Provider")

    realizes provider ~> auth
  end
rescue
  e -> e
end
# This succeeds perfectly because the contract is fully satisfied
uml_ok =
  uml strict_contract_validation: true do
    auth =
      behavior("AuthProvider") do
        function :verify, 1
      end

    provider =
      struct("Provider") do
        function :verify, 1
      end

    realizes provider ~> auth
  end

Themed Visualizations

All built-in presentation palettes are fully optimized to auto-skin each component type dynamically, applying custom matching header colors, text contrasts, borders, and line properties:

# Let's inspect the Ocean theme on our UML architecture
height = "600px"

tabs = [
  {"Default", Kino.VizJS.render(UML.to_dot(uml), height: height)},
  {"Ocean", Kino.VizJS.render(UML.to_dot(uml, theme: :ocean), height: height)},
  {"Dark", Kino.VizJS.render(UML.to_dot(uml, theme: :dark), height: height)}
]

Kino.Layout.tabs(tabs)

Dynamic Lens Queries (Choreo.Viewable)

Since Choreo.UML natively implements the Choreo.Viewable protocol, you can use the complete lens suite (Choreo.View) to query, filter, and focus on software compartments interactively:

# Focus only on the 'user' struct and its immediate dependencies
focused_uml = Choreo.View.focus(uml, :user)

Kino.VizJS.render(UML.to_dot(focused_uml))

Zoom levels filter by component type:

  • Level 0: Behaviors, protocols, and interfaces only
  • Level 1: + Classes
  • Level 2+: Everything (including structs)
zoomed = Choreo.View.zoom(uml, level: 0)
Kino.VizJS.render(UML.to_dot(zoomed))

Topological & Static Code Analysis

Choreo.UML.Analysis provides a comprehensive static analysis suite to verify system integrity, calculate coupling, and flag behavior/protocol realization issues:

1. Dependency Cycle Detection

Circular references between Elixir modules slow down compilation because they trigger massive recompilation cascades. Detect them instantly:

# Introducing a circular dependency using Lab DSL: user -> auth_provider -> cache -> user
circular_uml =
  uml do
    user =
      struct("User", id: :user) do
        field :id, :integer
      end

    auth =
      behavior("AuthProvider", id: :auth_provider) do
        function :verify, 1
      end

    cache =
      interface("Cache", id: :cache) do
        function :get, 1
      end

    realizes user ~> auth, "implements"
    depends auth ~> cache, "uses"
    depends cache ~> user, "references"
  end

Analysis.cycles(circular_uml)
#=> [[:auth_provider, :cache, :user]]

2. Broken Behavior & Protocol Contracts

Flag if a struct/class claims to implement a behavior contract but forgets some methods or has arity mismatches:

# Struct 'broken_user' claims to implement 'auth_provider' but is missing the 'verify/1' function
broken_uml =
  uml do
    auth =
      behavior("AuthProvider", id: :auth_provider) do
        function :verify, 1
      end

    broken_user = struct("BrokenUser", id: :broken_user)

    realizes broken_user ~> auth
  end

Analysis.broken_contracts(broken_uml)
#=> [{:broken_user, :auth_provider, [%{name: "verify", arity: 1}]}]

3. Coupling & Stability Metrics (Robert C. Martin's Metrics)

Evaluate Afferent Coupling ($C_a$), Efferent Coupling ($C_e$), and Instability ($I$):

Note on parallel edges: Coupling metrics collapse parallel relationships into a single edge before counting degrees.

Analysis.coupling_metrics(uml)

Cheat Sheet

Lab DSL Syntax

Syntax Description
uml do ... end Define a UML diagram
uml strict_contract_validation: true do ... end Define UML with strict contract checking
user = struct("User") do ... end Declare a struct with fields/functions
auth = behavior("AuthProvider") do ... end Declare a behavior with callbacks
cache = interface("Cache") do ... end Declare an interface contract
repo = protocol("RepoProtocol") do ... end Declare a protocol contract
field :id, :integer, visibility: :public Declare a field with visibility
function :verify, 1, return: :ok_error Declare a function with arity/return type
private(field(:email, :string)) Wrap field in visibility modifier
realizes user ~> auth, "implements" Adopting/implementing a behavior/protocol
inherits child ~> base, "extends" Class inheritance or specialization
depends service ~> cache, "calls" Runtime dependency or invocation
has order ~> item, "owns" Composition / association relationship
`service ~> repo > depends("calls")`
`service ~> cache > type(:associates)`

Programmatic Pipe API & Analysis

Task / Feature Command
Create UML Diagram UML.new/1 (Opts: :strict, :strict_contract_validation)
Add Struct/Behavior/Class UML.add_class/3 (Opts: :type, :fields, :functions, :label)
Add Connection Relationship UML.add_relationship/4 (Opts: :type, :label)
Render Styled DOT Graphviz UML.to_dot/2 (Opts: :theme, :direction, :highlighted_nodes)
Render Native Mermaid Class UML.to_mermaid/2 (Opts: syntax: :class_diagram, :direction)
Filter Graph via Lens focus Choreo.View.focus/3
Zoom Diagram by Tier Choreo.View.zoom/2
Detect circular cycles Analysis.cycles/1
Verify behavior contracts Analysis.broken_contracts/1
Compute coupling metrics Analysis.coupling_metrics/1
Check Law of Demeter Analysis.law_of_demeter_violations/1
Validate diagram Analysis.validate/1
Find affected classes Analysis.affected_by/2
Find class dependencies Analysis.depends_on/2
Find redundant relationships Analysis.transitive_reduction/1