mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
feat: generate mermaid entity relationship diagrams from a given api (#376)
This commit is contained in:
parent
6f5518de5c
commit
3d2f8277ad
3 changed files with 221 additions and 1 deletions
166
lib/ash/api/diagram/diagram.ex
Normal file
166
lib/ash/api/diagram/diagram.ex
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
defmodule Ash.Api.Info.Diagram do
|
||||||
|
@moduledoc """
|
||||||
|
Generate Mermaid diagrams from a specified API.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
We can't easily model Ash relationships with Mermaid diagrams
|
||||||
|
because they are unidirectional and could be asymmetric.
|
||||||
|
Mermaid assumes symmetrical, biredirectional relationships.
|
||||||
|
If we try to model all unidirectional realtionships as separate
|
||||||
|
lines in the diagram it gets very hard to read very quickly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@indent " "
|
||||||
|
|
||||||
|
@default_opts indent: @indent
|
||||||
|
|
||||||
|
defp resource_name(resource) do
|
||||||
|
resource
|
||||||
|
|> Ash.Resource.Info.short_name()
|
||||||
|
|> to_string()
|
||||||
|
|> Macro.camelize()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp short_module(module) do
|
||||||
|
module
|
||||||
|
|> Module.split()
|
||||||
|
|> List.last()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalise_relationships(api) do
|
||||||
|
for resource <- Ash.Api.Info.resources(api) do
|
||||||
|
for relationship <- Ash.Resource.Info.relationships(resource) do
|
||||||
|
[relationship.source, relationship.destination]
|
||||||
|
|> Enum.sort()
|
||||||
|
|> List.to_tuple()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|> Enum.flat_map(& &1)
|
||||||
|
|> Enum.uniq()
|
||||||
|
|> Enum.sort()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp aggregate_type(resource, aggregate) do
|
||||||
|
attribute_type =
|
||||||
|
if aggregate.field do
|
||||||
|
related = Ash.Resource.Info.related(resource, aggregate.relationship_path)
|
||||||
|
Ash.Resource.Info.attribute(related, aggregate.field).type
|
||||||
|
end
|
||||||
|
|
||||||
|
Ash.Query.Aggregate.kind_to_type(aggregate.kind, attribute_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
# default to one to one to just show connection
|
||||||
|
defp rel_type, do: "||--||"
|
||||||
|
|
||||||
|
defp short_type({:array, t}), do: "ArrayOf#{short_module(t)}"
|
||||||
|
defp short_type(t), do: short_module(t)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generates a Mermaid Entity Relationship Diagram for a given API.
|
||||||
|
|
||||||
|
Shows only public attributes, calculations, aggregates and actions.
|
||||||
|
Shows a one-to-one line for relationships as enumerating all unidirectional
|
||||||
|
relationships is far too noisy.
|
||||||
|
"""
|
||||||
|
def mermaid_er_diagram(api, opts \\ @default_opts) do
|
||||||
|
indent = opts[:indent] || @indent
|
||||||
|
|
||||||
|
resources =
|
||||||
|
for resource <- Ash.Api.Info.resources(api) do
|
||||||
|
attrs = Ash.Resource.Info.public_attributes(resource)
|
||||||
|
calcs = Ash.Resource.Info.public_calculations(resource)
|
||||||
|
aggs = Ash.Resource.Info.public_aggregates(resource)
|
||||||
|
|
||||||
|
contents =
|
||||||
|
[
|
||||||
|
join_template(attrs, indent, &"#{short_type(&1.type)} #{&1.name}"),
|
||||||
|
join_template(calcs, indent, &"#{short_type(&1.type)} #{&1.name}"),
|
||||||
|
join_template(aggs, indent, &"#{aggregate_type(resource, &1)} #{&1.name}")
|
||||||
|
]
|
||||||
|
|> Enum.reject(&(&1 == ""))
|
||||||
|
|> Enum.join("\n")
|
||||||
|
|
||||||
|
"""
|
||||||
|
#{indent}#{resource_name(resource)} {
|
||||||
|
#{contents}
|
||||||
|
#{indent}}
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|> Enum.join()
|
||||||
|
|
||||||
|
relationships =
|
||||||
|
for {src, dest} <- normalise_relationships(api) do
|
||||||
|
~s(#{indent}#{resource_name(src)} #{rel_type()} #{resource_name(dest)} : "")
|
||||||
|
end
|
||||||
|
|> Enum.join("\n")
|
||||||
|
|
||||||
|
"""
|
||||||
|
erDiagram
|
||||||
|
#{resources}
|
||||||
|
#{relationships}
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp class_short_type({:array, t}), do: "#{short_module(t)}[]"
|
||||||
|
defp class_short_type(t), do: short_module(t)
|
||||||
|
|
||||||
|
defp join_template(list, indent, template_fn) do
|
||||||
|
Enum.map_join(list, "\n", fn item -> "#{indent}#{indent}#{template_fn.(item)}" end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generates a Mermaid Class Diagram for a given API.
|
||||||
|
|
||||||
|
Shows only public attributes, calculations, aggregates and actions.
|
||||||
|
Shows a connecting line for relationships with the type of relationship
|
||||||
|
indicated in the attribute list.
|
||||||
|
"""
|
||||||
|
def mermaid_class_diagram(api, opts \\ @default_opts) do
|
||||||
|
indent = opts[:indent] || @indent
|
||||||
|
|
||||||
|
resources =
|
||||||
|
for resource <- Ash.Api.Info.resources(api) do
|
||||||
|
attrs = Ash.Resource.Info.public_attributes(resource)
|
||||||
|
calcs = Ash.Resource.Info.public_calculations(resource)
|
||||||
|
aggs = Ash.Resource.Info.public_aggregates(resource)
|
||||||
|
actions = Ash.Resource.Info.actions(resource)
|
||||||
|
relationships = Ash.Resource.Info.public_relationships(resource)
|
||||||
|
|
||||||
|
contents =
|
||||||
|
[
|
||||||
|
join_template(attrs, indent, &"#{class_short_type(&1.type)} #{&1.name}"),
|
||||||
|
join_template(calcs, indent, &"#{class_short_type(&1.type)} #{&1.name}"),
|
||||||
|
join_template(aggs, indent, &"#{aggregate_type(resource, &1)} #{&1.name}"),
|
||||||
|
join_template(
|
||||||
|
relationships,
|
||||||
|
indent,
|
||||||
|
&"#{resource_name(&1.destination)}#{if &1.cardinality == :many, do: "[]", else: ""} #{&1.name}"
|
||||||
|
),
|
||||||
|
join_template(actions, indent, &"#{&1.name}()")
|
||||||
|
]
|
||||||
|
|> Enum.reject(&(&1 == ""))
|
||||||
|
|> Enum.join("\n")
|
||||||
|
|
||||||
|
"""
|
||||||
|
#{indent}class #{resource_name(resource)} {
|
||||||
|
#{contents}
|
||||||
|
#{indent}}
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|> Enum.join()
|
||||||
|
|
||||||
|
relationships =
|
||||||
|
for {src, dest} <- normalise_relationships(api) do
|
||||||
|
~s(#{indent}#{resource_name(src)} -- #{resource_name(dest)})
|
||||||
|
end
|
||||||
|
|> Enum.join("\n")
|
||||||
|
|
||||||
|
"""
|
||||||
|
classDiagram
|
||||||
|
#{resources}
|
||||||
|
#{relationships}
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
|
@ -11,7 +11,7 @@ defmodule Ash.Resource.Change.RelateActor do
|
||||||
type: :atom
|
type: :atom
|
||||||
],
|
],
|
||||||
allow_nil?: [
|
allow_nil?: [
|
||||||
doc: "Wether or not to allow the actor to be nil, in which case nothing will happen.",
|
doc: "Whether or not to allow the actor to be nil, in which case nothing will happen.",
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
default: false
|
default: false
|
||||||
]
|
]
|
||||||
|
|
54
test/api/diagram_test.exs
Normal file
54
test/api/diagram_test.exs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
defmodule Ash.Test.Api.Info.DiagramTest do
|
||||||
|
@moduledoc false
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
test "generate a mermaid entity relationship diagram from an Api" do
|
||||||
|
assert Ash.Api.Info.Diagram.mermaid_er_diagram(Ash.Test.Support.Flow.Api) == """
|
||||||
|
erDiagram
|
||||||
|
User {
|
||||||
|
UUID id
|
||||||
|
String first_name
|
||||||
|
String last_name
|
||||||
|
String email
|
||||||
|
}
|
||||||
|
Org {
|
||||||
|
UUID id
|
||||||
|
String name
|
||||||
|
}
|
||||||
|
|
||||||
|
Org ||--|| User : ""
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
test "generate a mermaid class diagram from an Api" do
|
||||||
|
assert Ash.Api.Info.Diagram.mermaid_class_diagram(Ash.Test.Support.Flow.Api) == """
|
||||||
|
classDiagram
|
||||||
|
class User {
|
||||||
|
UUID id
|
||||||
|
String first_name
|
||||||
|
String last_name
|
||||||
|
String email
|
||||||
|
Org org
|
||||||
|
destroy()
|
||||||
|
read()
|
||||||
|
for_org()
|
||||||
|
create()
|
||||||
|
update()
|
||||||
|
approve()
|
||||||
|
unapprove()
|
||||||
|
}
|
||||||
|
class Org {
|
||||||
|
UUID id
|
||||||
|
String name
|
||||||
|
User[] users
|
||||||
|
destroy()
|
||||||
|
update()
|
||||||
|
read()
|
||||||
|
create()
|
||||||
|
by_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
Org -- User
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue