Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
[![Hexdocs badge](https://img.shields.io/badge/docs-hexdocs-purple)](https://hexdocs.pm/clarity)

<!-- ex_doc_ignore_start -->

# Clarity

<!-- ex_doc_ignore_end -->

⚠️ **Alpha Notice**: Clarity is currently in an **alpha state**. APIs and features
may change rapidly, and things may break. Feedback and contributions are very
welcome!


Clarity is an interactive introspection and visualization tool for Elixir projects.
It automatically discovers and visualizes applications, domains, resources,
modules, and their relationships, giving you a navigable graph interface
Expand Down Expand Up @@ -45,6 +46,11 @@ to your `mix.exs` dependencies.
- **[ash_diagram](https://hex.pm/packages/ash_diagram)** – Provides mermaid
diagrams for Ash

> **Library authors:** see
> [Integrating a Library with Clarity](documentation/how_to/integrate-from-a-library.md)
> for the convention used to ship Clarity content from inside your own
> library — no host-side configuration required on the consumer's part.

## Installation

### Igniter
Expand All @@ -53,6 +59,12 @@ to your `mix.exs` dependencies.
mix igniter.install clarity
```

If you want to include Ash diagramming and visualisation support then also install ash_diagram:

```bash
mix igniter.install clarity ash_diagram
```

### Manual

The package can be installed by adding `clarity` to your list of dependencies
Expand All @@ -67,12 +79,14 @@ end
```

Router:

```elixir
import Clarity.Router
clarity("/clarity")
```

<!-- ex_doc_ignore_start -->

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm).The docs can be found at
<https://hexdocs.pm/clarity>.
Expand All @@ -89,6 +103,7 @@ This starts a Phoenix server at http://localhost:4000 with live reload enabled.

See [usage-rules/development.md](usage-rules/development.md) for more details on
testing, asset building, and code quality tools.

<!-- ex_doc_ignore_end -->

## License
Expand Down
180 changes: 180 additions & 0 deletions documentation/how_to/integrate-from-a-library.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Integrating a Library with Clarity

This guide is for **library authors** who want their library's
vertex types, content providers, introspectors, or lensmakers to
appear in Clarity automatically whenever a consumer has both
Clarity and the library installed.

It describes the "wrap what you have" pattern: a library ships a
thin adapter guarded by `Code.ensure_loaded?/1`, registers it in
its own `application/0` environment, and Clarity auto-discovers
it on startup. The host project doesn't need to add any
`config :clarity_content_providers` line — or anything at all —
beyond the two `deps` entries.

> **Writing a provider in your own application?** The
> application-author path is simpler: register providers directly
> via `config :my_app, :clarity_content_providers, [...]` in your
> `config/config.exs`. Library-side integration, described below,
> is the right move for a reusable library but not for a
> Phoenix/Ash application.

## Why library-side integration?

Clarity content is owned most naturally by the library whose
concepts it visualises. Co-locating the adapter with the DSL it
describes gives four properties that a central bridge library
cannot match:

- **Versioning moves together.** A DSL change and its diagram
change live in the same commit, so the UI never drifts from the
DSL.
- **No central bottleneck.** There's no single library that has to
accept a pull request for every new extension in the ecosystem.
- **Discoverability.** Consumers of the library find the Clarity
integration in the library's own README, CHANGELOG, and HexDocs.
- **Scoping is automatic.** If the library is not used in the host
project, no module is compiled; if Clarity is not installed,
the Clarity adapter is skipped at compile time.

**Reserve central packages such as
[`ash_diagram`](https://hex.pm/packages/ash_diagram) for genuinely
cross-cutting visualisations** — entity-relationship diagrams
spanning a whole domain, C4-style architecture overviews, anything
that is not owned by a single extension. Extension-specific
visualisations belong in the extension itself.

## The three-part contract

A library integrates with Clarity by doing three things in concert.
Each is independently harmless if Clarity is absent.

### 1. Guard the adapter module with `Code.ensure_loaded?/1`

Wrap the entire module definition in a `with`-clause so the module
is only compiled when the Clarity behaviour it depends on is loaded:

```elixir
with {:module, _} <- Code.ensure_loaded(Clarity.Content) do
defmodule MyLibrary.Clarity.MyDiagram do
@behaviour Clarity.Content

alias Clarity.Vertex

@impl Clarity.Content
def name, do: "My Diagram"

@impl Clarity.Content
def description, do: "A short description of what this renders."

@impl Clarity.Content
def applies?(%Vertex.Ash.Resource{resource: resource}, _lens),
do: relevant?(resource)

def applies?(_vertex, _lens), do: false

@impl Clarity.Content
def render_static(%Vertex.Ash.Resource{resource: resource}, _lens) do
{:mermaid, fn _props -> MyLibrary.Charts.diagram(resource) end}
end

defp relevant?(resource) do
# your predicate — e.g. "does this resource use my extension?"
MyLibrary.Extension in Spark.extensions(resource)
end
end
end
```

The same pattern applies for the other extension points: guard on
`Clarity.Introspector` for introspectors, on `Clarity.Vertex` for
vertex types, and on `Clarity.Perspective.Lensmaker` for lensmakers.

### 2. Register the module via `application/0` in `mix.exs`

Expose the adapter through your library's environment. Clarity
walks every loaded application's environment at startup and
aggregates registered providers — no host-side configuration
required.

```elixir
# mix.exs
def application do
[
extra_applications: [:logger],
env: [
clarity_content_providers: [
MyLibrary.Clarity.MyDiagram
]
]
]
end
```

The same environment keys exist for the other extension points:

| Provider type | Environment key |
| ----------------- | --------------------------------- |
| Content providers | `:clarity_content_providers` |
| Introspectors | `:clarity_introspectors` |
| Lensmakers | `:clarity_perspective_lensmakers` |

### 3. Declare Clarity as an optional dependency

In `defp deps/0`:

```elixir
defp deps do
[
# ...your usual deps...
{:clarity, "~> 0.4", optional: true}
]
end
```

`optional: true` gives you three properties at once:

- Consumers who install your library without Clarity get no
Clarity code — the guarded module never compiles.
- Consumers who install both get the integration automatically.
- `mix deps.get` in your own library resolves Clarity so you can
develop and test the adapter locally.

## Recommended module naming

Use `YourLibrary.Clarity.*` as the namespace for Clarity-facing
modules. For example:

- `MyLibrary.Clarity.SomeDiagram` — a content provider
- `MyLibrary.Clarity.Introspector` — a custom introspector
- `MyLibrary.Clarity.Vertex.SomeThing` — a custom vertex type

Avoid the older `ClarityContent.*` naming used in some early
integrations: it is redundant with the library namespace (the
Clarity context is already implicit) and leaves no room for
introspectors, vertex types, or lensmakers in the same tree.

## Listing under "Third-Party Libraries"

Once your library ships Clarity integration, open a pull request
against the Clarity README adding it to the Third-Party Libraries
section. A one-liner is enough:

```markdown
- **[my_library](https://hex.pm/packages/my_library)** – Short
description of what the Clarity integration visualises.
```

## Worked examples

- [`ash_diagram`](https://hex.pm/packages/ash_diagram) ships five
Clarity content providers (`ErDiagram`, `ClassDiagram`,
`ArchitectureDiagram`, `PolicyDiagram`, `PolicySimulation`), each
guarded with `Code.ensure_loaded?/1` and registered via
`application/0`. Read its `mix.exs` and
`lib/ash_diagram/clarity_content/*.ex` for a reference
implementation of the pattern described above.
- [`ash_state_machine`](https://hex.pm/packages/ash_state_machine)
ships a single state-diagram content provider at
`AshStateMachine.Clarity.StateMachineDiagram`, demonstrating the
minimal-surface-area version of the pattern.
6 changes: 3 additions & 3 deletions lib/clarity/components/tooltip_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ defmodule Clarity.TooltipComponent do
socket = assign(socket, assigns)

# Only reset stream when breadcrumbs change to avoid DOM churn
if old_breadcrumbs != assigns[:breadcrumbs] do
if old_breadcrumbs == assigns[:breadcrumbs] do
{:ok, socket}
else
preload_vertices = compute_preload_vertices(socket.assigns)
{:ok, stream(socket, :tooltips, preload_vertices, reset: true)}
else
{:ok, socket}
end
end

Expand Down
20 changes: 5 additions & 15 deletions lib/clarity/content/ash/action_overview.ex
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,7 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do
"## Arguments\n\n",
"| Name | Type | Description | Required | Allow Nil |\n",
"| --- | --- | --- | --- | --- |\n",
action.arguments
|> Enum.map(&argument_row/1)
|> Enum.intersperse(""),
Enum.map_intersperse(action.arguments, "", &argument_row/1),
"\n\n"
]
end
Expand Down Expand Up @@ -216,9 +214,7 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do
else
[
"## Changes\n\n",
changes
|> Enum.map(&format_change/1)
|> Enum.intersperse("\n"),
Enum.map_intersperse(changes, "\n", &format_change/1),
"\n\n"
]
end
Expand Down Expand Up @@ -331,9 +327,7 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do
else
[
"## Preparations\n\n",
preparations
|> Enum.map(&format_preparation/1)
|> Enum.intersperse("\n"),
Enum.map_intersperse(preparations, "\n", &format_preparation/1),
"\n\n"
]
end
Expand Down Expand Up @@ -487,16 +481,12 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do

@spec format_atom_list([atom()]) :: iodata()
defp format_atom_list(list) do
list
|> Enum.map(&["`", to_string(&1), "`"])
|> Enum.intersperse(", ")
Enum.map_intersperse(list, ", ", &["`", to_string(&1), "`"])
end

@spec format_module_list([module()]) :: iodata()
defp format_module_list(list) do
list
|> Enum.map(&inspect/1)
|> Enum.intersperse(", ")
Enum.map_intersperse(list, ", ", &inspect/1)
end

@spec clean_description(String.t() | nil) :: String.t()
Expand Down
12 changes: 3 additions & 9 deletions lib/clarity/content/ash/aggregate_overview.ex
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,7 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do

@spec format_relationship_path(list(atom()), Ash.Resource.t()) :: iodata()
defp format_relationship_path(path, resource) when is_list(path) do
path
|> Enum.map(fn rel_name ->
Enum.map_intersperse(path, " → ", fn rel_name ->
[
"[`",
to_string(rel_name),
Expand All @@ -189,7 +188,6 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do
")"
]
end)
|> Enum.intersperse(" → ")
end

@spec format_field_link(atom(), list(atom()), Ash.Resource.t(), boolean()) :: iodata()
Expand Down Expand Up @@ -235,9 +233,7 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do
"## Join Filters\n\n",
"| Relationship Path | Filter |\n",
"| --- | --- |\n",
join_filters
|> Enum.map(&join_filter_row(&1, resource))
|> Enum.intersperse(""),
Enum.map_intersperse(join_filters, "", &join_filter_row(&1, resource)),
"\n\n"
]
end
Expand Down Expand Up @@ -285,9 +281,7 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do
"## Return Type Constraints\n\n",
"| Constraint | Value |\n",
"| --- | --- |\n",
constraints
|> Enum.map(&constraint_row/1)
|> Enum.intersperse(""),
Enum.map_intersperse(constraints, "", &constraint_row/1),
"\n\n"
]
end
Expand Down
4 changes: 1 addition & 3 deletions lib/clarity/content/ash/attribute_overview.ex
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,7 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do
"## Constraints\n\n",
"| Constraint | Value |\n",
"| --- | --- |\n",
constraints
|> Enum.map(&constraint_row/1)
|> Enum.intersperse(""),
Enum.map_intersperse(constraints, "", &constraint_row/1),
"\n\n"
]
end
Expand Down
12 changes: 3 additions & 9 deletions lib/clarity/content/ash/calculation_overview.ex
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,7 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do
"## Arguments\n\n",
"| Name | Type | Description | Required | Default |\n",
"| --- | --- | --- | --- | --- |\n",
arguments
|> Enum.map(&argument_row/1)
|> Enum.intersperse(""),
Enum.map_intersperse(arguments, "", &argument_row/1),
"\n\n"
]
end
Expand Down Expand Up @@ -217,9 +215,7 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do
[
"## Dependencies\n\n",
"This calculation requires the following fields/relationships to be loaded:\n\n",
load
|> Enum.map(&format_dependency/1)
|> Enum.intersperse("\n"),
Enum.map_intersperse(load, "\n", &format_dependency/1),
"\n\n"
]
end
Expand All @@ -241,9 +237,7 @@ with {:module, Ash} <- Code.ensure_loaded(Ash) do
"## Return Type Constraints\n\n",
"| Constraint | Value |\n",
"| --- | --- |\n",
constraints
|> Enum.map(&constraint_row/1)
|> Enum.intersperse(""),
Enum.map_intersperse(constraints, "", &constraint_row/1),
"\n\n"
]
end
Expand Down
Loading
Loading