Clarity is a code visualization and documentation tool for Elixir projects. It introspects your codebase and builds a graph representation that can be viewed through different "lenses" (filtered perspectives).
Clarity is built around four main extension points:
- Vertex Types - Define new node types in the graph (modules, resources, custom entities)
- Introspectors - Analyze code to discover and add vertices/edges to the graph
- Content Providers - Display information about vertices in the UI
- Lensmakers - Create filtered views of the graph for different audiences
All extensions are registered via application configuration, allowing third-party libraries to seamlessly extend Clarity's capabilities.
For detailed guides on creating each type of extension, see the sub-rules:
- Creating Vertex Types: See
clarity:vertex-typesusage rules - Creating Introspectors: See
clarity:introspectorsusage rules - Creating Content Providers: See
clarity:content-providersusage rules - Creating Lenses: See
clarity:lensmakersusage rules
Extensions are registered per-application in your config files:
# In config/config.exs or config/runtime.exs
config :my_app, :clarity_introspectors, [
MyApp.CustomIntrospector
]
config :my_app, :clarity_content_providers, [
MyApp.CustomContent
]
config :my_app, :clarity_perspective_lensmakers, [
MyApp.CustomLensmaker
]Clarity automatically discovers and loads extensions from all loaded applications.
# Filter which applications to introspect
config :clarity, :introspector_applications,
[:my_app, :phoenix, :ash]
# Set default lens
config :clarity, :default_perspective_lens, "debug"
# Configure editor integration
config :clarity, :editor,
"code --goto __FILE__:__LINE__:__COLUMN__"
# Auto-start on application boot
config :clarity, :auto_start?, true
# Custom cache path
config :clarity, :cache_path, "/custom/path"Clarity uses a tuple-based query syntax for filtering vertices:
# Equality
{:==, :vertex_type, Clarity.Vertex.Module}
# Inequality
{:!=, :vertex_id, "some-id"}
# Membership
{:in, :vertex_type,
[Clarity.Vertex.Module, Clarity.Vertex.Application]}# AND - both conditions must be true
{:and, query1, query2}
# OR - either condition must be true
{:or, query1, query2}
# NOT - inverts the condition
{:not, query}:vertex_id # Result of Clarity.Vertex.id(vertex)
:vertex_type # vertex.__struct__The Clarity.Graph.Filter module provides helpers for common
filter patterns:
alias Clarity.Graph.Filter
# Filter by vertex type
Filter.vertex_type([Clarity.Vertex.Application])
# Filter by distance from a vertex
Filter.within_steps(center_vertex, max_outgoing_steps,
max_incoming_steps)
# Filter by reachability
Filter.reachable_from([source_vertex1, source_vertex2])Use Clarity.Vertex.Util.id/2 to create consistent vertex IDs:
alias Clarity.Vertex.Util
# Single identifier component
Util.id(MyVertex, "component")
# => "Elixir.MyVertex:component"
# Multiple identifier components
Util.id(MyVertex, ["parent", "child", "field"])
# => "Elixir.MyVertex:parent:child:field"Use Code.ensure_loaded/1 before function_exported?/3:
def introspect_vertex(%Vertex.Module{module: module}, graph) do
case Code.ensure_loaded(module) do
{:module, ^module} ->
if function_exported?(module, :__info__, 1) do
# Process the module
end
_ ->
{:ok, []}
end
endWhen an introspector needs data that isn't in the graph yet:
def introspect_vertex(vertex, graph) do
case Clarity.Graph.get_vertex(graph, needed_vertex_id) do
nil ->
# Dependency not met, will retry later
{:error, :unmet_dependencies}
dependency_vertex ->
# Process with dependency
{:ok, entries}
end
enddefmodule MyApp.MyVertexTest do
use ExUnit.Case, async: true
alias Clarity.Vertex
alias MyApp.MyVertex
setup do
vertex = %MyVertex{field: "value"}
{:ok, vertex: vertex}
end
describe inspect(&Vertex.id/1) do
test "returns unique identifier", %{vertex: vertex} do
assert Vertex.id(vertex) == "expected-id"
end
end
describe inspect(&Vertex.type_label/1) do
test "returns correct type label", %{vertex: vertex} do
assert Vertex.type_label(vertex) == "My Type"
end
end
endVertex IDs must be globally unique across the entire graph. Include enough identifying context in your vertex struct and ID generation:
# Bad - multiple resources can have :update action
%Ash.Action{name: :update}
Util.id(@for, [:update])
# Multiple actions will have same ID - conflict!
# Good - include parent context
%Ash.Action{name: :update, resource: MyApp.User}
Util.id(@for, [MyApp.User, :update])
# Each resource's :update action has unique ID
# Bad - ambiguous field name
%CustomField{name: "email"}
Util.id(@for, ["email"])
# Good - include parent entity
%CustomField{name: "email", parent: MyApp.User}
Util.id(@for, [MyApp.User, "email"])lib/clarity/
├── vertex/ # Vertex type definitions
├── introspector/ # Introspector implementations
├── content/ # Content providers
├── perspective/
│ └── lensmaker/ # Lensmaker implementations
├── graph/
│ └── filter.ex # Filter helpers
├── vertex.ex # Vertex protocol
├── introspector.ex # Introspector behavior
├── content.ex # Content behavior
└── config.ex # Configuration management
- Always implement required protocols/behaviors completely
- Provide clear error messages with context
- Test protocol implementations thoroughly
- Use descriptive names for vertices and relationships
- Document your extensions well
- Follow Elixir and Clarity conventions
- Use
@impl ModuleName(not@impl true) - Include
@specfor all public functions except callbacks