Powered by AppSignal & Oban Pro

Choreo UML: Comprehensive Walkthrough

livebooks/uml_walkthrough.livemd

Choreo UML: Comprehensive Walkthrough

Mix.install([
  {:choreo, "~> 0.8.0"},
  {:kino_vizjs, "~> 0.8.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).

alias Choreo.UML

# Initialize a standard user and authentication architecture
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")

Styled Graphviz Rendering

Graphviz DOT rendering constructs high-fidelity three-compartment HTML-like tables standard in UML tools:

UML.to_dot(uml)
|> Kino.VizJS.render()

Native Mermaid.js classDiagram Rendering

You can output directly to Mermaid’s native, highly structured class diagram syntax:

# Simply pass `syntax: :class_diagram`
UML.to_mermaid(uml, syntax: :class_diagram)
|> Kino.Mermaid.new()

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), :type (Optional), and :visibility (Optional: :public (+), :private (-), :protected (#), or nil).
  • Functions: Supports :name (Required), :arity (Optional), :return (Optional), and :visibility (Optional).
# Validation will trigger a clean schema error if fields are invalid
try do
  UML.new()
  |> UML.add_class(:bad_module, fields: [
    %{visibility: :public} # Missing name!
  ])
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 (`– >`) Class inheritance or adopting a behavior.
:realizes Hollow Dashed (`.. >`) Adopting/implementing an Elixir protocol.
:associates Solid Solid (-->) Composition (e.g. struct nesting/embedding).
:depends Open Dashed (..>) Run-time dependency or function calling invocation.
# Quick example of behavior inheritance
inheritance_uml =
  UML.new()
  |> UML.add_class(:base_handler, type: :class)
  |> UML.add_class(:http_handler, type: :class)
  |> UML.add_relationship(:http_handler, :base_handler, type: :inherits, label: "inherits_from")

UML.to_mermaid(inheritance_uml, syntax: :class_diagram)
|> Kino.Mermaid.new()

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 user missing the verify/1 function
try do
  UML.new(strict_contract_validation: true)
  |> UML.add_class(:auth_provider,
    type: :behavior,
    functions: [%{name: "verify", arity: 1}]
  )
  |> UML.add_class(:provider, type: :struct)
  |> UML.add_relationship(:provider, :auth_provider, type: :realizes)
rescue
  e -> e
end
# This succeeds perfectly because the contract is fully satisfied
uml_ok =
  UML.new(strict_contract_validation: true)
  |> UML.add_class(:auth_provider,
    type: :behavior,
    functions: [%{name: "verify", arity: 1}]
  )
  |> UML.add_class(:provider,
    type: :struct,
    functions: [%{name: "verify", arity: 1}]
  )
  |> UML.add_relationship(:provider, :auth_provider, type: :realizes)

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
UML.to_dot(uml, theme: :ocean)
|> Kino.VizJS.render()
# Let's inspect the Dark theme
UML.to_dot(uml, theme: :dark)
|> Kino.VizJS.render()

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)

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

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: user -> auth_provider -> cache -> user
circular_uml = 
  uml
  |> UML.add_relationship(:cache, :user, type: :depends, label: "references")

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

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.new()
  |> UML.add_class(:auth_provider,
    type: :behavior,
    functions: [%{name: "verify", arity: 1}]
  )
  |> UML.add_class(:broken_user, type: :struct)
  |> UML.add_relationship(:broken_user, :auth_provider, type: :realizes)

Choreo.UML.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$):

Choreo.UML.Analysis.coupling_metrics(uml)

Cheat Sheet

Task / Feature Command
Create UML Diagram UML.new/0
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/2
Detect circular cycles Choreo.UML.Analysis.cycles/1
Verify behavior contracts Choreo.UML.Analysis.broken_contracts/1
Compute coupling metrics Choreo.UML.Analysis.coupling_metrics/1