mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 05:23:03 +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
|
||||
],
|
||||
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,
|
||||
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