Mix.install([
{:kino, "~> 0.6.2"},
{:kino_vega_lite, "~> 0.1.1"},
{:ecto, "~> 3.8"},
{:libgraph, "~> 0.16.0"}
])
defmodule EctoIntrospect do
def introspect(struct) do
introspect_fields(struct, assocs(struct))
end
def introspect_fields(struct, assocs) do
struct_name = inspect(struct)
struct.__schema__(:fields)
|> Enum.map(fn field -> {field, struct.__schema__(:type, field)} end)
|> Enum.map(fn {field, type} ->
case Enum.find(assocs, fn {_, _, owner_key, _, _} -> field == owner_key end) do
nil ->
{struct_name, field, type}
{field, inner_struct, type, relationship, cardinality} ->
case halt?(assocs(inner_struct), struct) do
true ->
nil
false ->
{struct_name, field, introspect(inner_struct), type, relationship, cardinality}
end
end
end)
|> Enum.reject(&is_nil/1)
end
defp assocs(struct) do
struct.__schema__(:associations)
|> Enum.map(&struct.__schema__(:association, &1))
|> Enum.map(&{&1.field, &1.related, &1.owner_key, &1.relationship, &1.cardinality})
end
def halt?(assocs, parent) do
Enum.any?(assocs, fn {_, assoc, _, _, _} -> assoc == parent end)
end
end
defmodule EctoVisualizeSchema do
def vis(struct) do
struct
|> EctoIntrospect.introspect()
|> Enum.reduce([], &mermaid/2)
|> Enum.uniq()
|> Enum.join("\n")
|> wrap()
|> Kino.Markdown.new()
end
defp mermaid({struct, field, type}, acc) do
class_def = "class #{struct}"
type_def = "#{struct} : #{type} #{field}"
acc ++ [class_def] ++ [type_def]
end
defp mermaid({struct, type, fields, field, relationship, cardinality}, acc) do
relantionship =
case {relationship, cardinality} do
{:parent, :one} -> "\"0\" <|-- \"1\""
{:parent, :many} -> "\"0\" <|-- \"*\""
{:child, :one} -> "\"0\" --|> \"1\""
{:child, :many} -> "\"0\" --|> \"*\""
_ -> ".."
end
linked_struct = fields |> hd() |> elem(0)
class_def = Enum.flat_map(fields, &mermaid(&1, []))
relationship_def = ["#{struct} #{relantionship} #{linked_struct}"]
type_def = "#{struct} : #{type} #{field}"
acc ++ class_def ++ relationship_def ++ [type_def]
end
defp wrap(spec),
do: """
```mermaid
classDiagram
#{spec}
```
"""
end
defmodule Organization do
use Ecto.Schema
schema "organizations" do
field(:name, :string)
has_one(:team, Team)
end
end
defmodule Team do
use Ecto.Schema
schema "teams" do
field(:name, :string)
has_many(:persons, Person)
end
end
defmodule Person do
use Ecto.Schema
schema "persons" do
field(:name, :string)
field(:age, :integer)
has_one(:team, Team)
belongs_to(:organization, Organization)
end
end
EctoVisualizeSchema.vis(Person)
I ran the exact same code, on a linked Ecto schema in my app, and get this error: