Skip to content

Commit eb90c16

Browse files
committed
wip: Start adding Mermaid flowcharts for Reactor
1 parent 5224269 commit eb90c16

File tree

12 files changed

+495
-11
lines changed

12 files changed

+495
-11
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"backoff",
55
"casted",
66
"Desugars",
7+
"indentify",
78
"lvalue",
89
"mappish",
910
"noreply",

lib/reactor.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ defmodule Reactor do
4949
undo: []
5050

5151
use Spark.Dsl, default_extensions: [extensions: [Dsl]]
52+
@behaviour Reactor.Mermaid.Render
5253

5354
@type context :: %{optional(atom) => any}
5455
@type context_arg :: Enumerable.t({atom, any})
@@ -219,4 +220,8 @@ defmodule Reactor do
219220
{:error, reason} -> raise reason
220221
end
221222
end
223+
224+
@doc false
225+
def to_mermaid(reactor, options) when is_struct(reactor, __MODULE__),
226+
do: Reactor.Mermaid.Render.Reactor.to_mermaid(reactor, options)
222227
end

lib/reactor/argument.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ defmodule Reactor.Argument do
55

66
defstruct description: nil, name: nil, source: nil, transform: nil
77

8+
@behaviour Reactor.Mermaid.Render
9+
810
alias Reactor.{Argument, Template}
911
import Reactor.Template, only: :macros
1012

@@ -219,6 +221,11 @@ defmodule Reactor.Argument do
219221
defguard has_sub_path(argument)
220222
when is_list(argument.source.sub_path) and argument.source.sub_path != []
221223

224+
@doc false
225+
@impl true
226+
def to_mermaid(argument, options),
227+
do: Reactor.Mermaid.Render.Argument.to_mermaid(argument, options)
228+
222229
defp validate_options!(options) when is_list(options),
223230
do: Spark.Options.validate!(options, @options)
224231

lib/reactor/mermaid.ex

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
defmodule Reactor.Mermaid do
2+
@moduledoc """
3+
Converts Reactors and their related entities into a Mermaid diagram.
4+
"""
5+
@options Spark.Options.new!(
6+
expand?: [
7+
type: :boolean,
8+
required: false,
9+
default: false,
10+
doc: "Whether or not to expand composed Reactors"
11+
],
12+
describe?: [
13+
type: :boolean,
14+
required: false,
15+
default: false,
16+
doc: "Whether or not to include descriptions, if available"
17+
],
18+
direction: [
19+
type: {:in, [:top_to_bottom, :bottom_to_top, :right_to_left, :left_to_right]},
20+
required: false,
21+
default: :top_to_bottom,
22+
doc: "The direction to render the flowchart"
23+
],
24+
indent: [
25+
type: :non_neg_integer,
26+
required: false,
27+
default: 0,
28+
doc: "How much to indent the resulting mermaid"
29+
]
30+
)
31+
32+
@type options :: unquote(Spark.Options.option_typespec(@options))
33+
34+
import __MODULE__.Utils
35+
36+
@doc """
37+
Convert the Reactor into Mermaid.
38+
39+
## Options
40+
41+
#{Spark.Options.docs(@options)}
42+
"""
43+
@spec to_mermaid(module | Reactor.t(), options) :: {:ok, iodata()} | {:error, any}
44+
def to_mermaid(reactor, options \\ [])
45+
46+
def to_mermaid(reactor, options) do
47+
with {:ok, options} <- Spark.Options.validate(options, @options) do
48+
do_to_mermaid(reactor, options)
49+
end
50+
end
51+
52+
@doc """
53+
Convert the Reactor into Mermaid
54+
55+
Raising version of `to_mermaid/2`
56+
"""
57+
@spec to_mermaid!(module | Reactor.t(), options) :: iodata | no_return()
58+
def to_mermaid!(reactor, options \\ []) do
59+
case to_mermaid(reactor, options) do
60+
{:ok, iodata} -> iodata
61+
{:error, reason} when is_exception(reason) -> raise reason
62+
{:error, reason} -> raise RuntimeError, reason
63+
end
64+
end
65+
66+
defp do_to_mermaid(reactor, options) when is_atom(reactor) do
67+
if Code.ensure_loaded?(reactor) && function_exported?(reactor, :spark_is, 0) &&
68+
reactor.spark_is() == Reactor do
69+
do_to_mermaid(reactor.reactor(), options)
70+
else
71+
{:error, ArgumentError.exception(message: "`reactor` argument is not a Reactor")}
72+
end
73+
end
74+
75+
defp do_to_mermaid(reactor, options) when is_struct(reactor, Reactor) do
76+
with {:ok, reactor} <- Reactor.Planner.plan(reactor) do
77+
mermaid = __MODULE__.Render.to_mermaid(reactor, indent(options))
78+
{:ok, indentify([["flowchart #{direction(options[:direction])}\n"] | mermaid], options)}
79+
end
80+
end
81+
82+
defp direction(:top_to_bottom), do: "TD"
83+
defp direction(:bottom_to_top), do: "BT"
84+
defp direction(:left_to_right), do: "LR"
85+
defp direction(:right_to_left), do: "RL"
86+
end

lib/reactor/mermaid/render.ex

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
defmodule Reactor.Mermaid.Render do
2+
@moduledoc """
3+
The behaviour for converting items into [Mermaid](https://mermaid.js.org/) charts.
4+
5+
Don't call this behaviour directly, instead use it via `Reactor.Mermaid`.
6+
"""
7+
8+
@doc """
9+
Convert something item into Mermaid
10+
11+
Options will have previously been validated by `Reactor.Mermaid.to_mermaid/2`.
12+
"""
13+
@callback to_mermaid(module | struct, Reactor.Mermaid.options()) :: iodata() | no_return
14+
15+
@doc """
16+
Convert something item into Mermaid
17+
18+
Options will have previously been validated by `Reactor.Mermaid.to_mermaid/2`.
19+
"""
20+
@spec to_mermaid(module | struct, Reactor.Mermaid.options()) :: iodata() | no_return
21+
def to_mermaid(module, options) when is_atom(module) do
22+
if Spark.implements_behaviour?(module, __MODULE__) do
23+
module.to_mermaid(module, options)
24+
else
25+
raise ArgumentError,
26+
"module `#{module}` does not implement the `#{inspect(__MODULE__)}` behaviour"
27+
end
28+
end
29+
30+
def to_mermaid(struct, options) when is_struct(struct) do
31+
module = struct.__struct__
32+
33+
if Spark.implements_behaviour?(module, __MODULE__) do
34+
module.to_mermaid(struct, options)
35+
else
36+
raise ArgumentError,
37+
"module `#{module}` does not implement the `#{inspect(__MODULE__)}` behaviour"
38+
end
39+
end
40+
end
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
defmodule Reactor.Mermaid.Render.Argument do
2+
@moduledoc false
3+
import Reactor.Mermaid.Utils
4+
alias Reactor.Argument
5+
require Argument
6+
7+
@doc false
8+
def to_mermaid(argument, options) when Argument.is_from_value(argument) do
9+
target_id =
10+
options
11+
|> Keyword.fetch!(:parent_step)
12+
|> Map.fetch!(:name)
13+
|> mermaid_id(:step)
14+
15+
source_id =
16+
argument.source.value
17+
|> mermaid_id(:value)
18+
19+
indentify(
20+
"""
21+
#{source_id}{{"`#{md_escape(inspect(argument.source.value, pretty: true))}`"}}}
22+
#{do_argument_link(source_id, target_id, argument, options)}
23+
""",
24+
options
25+
)
26+
end
27+
28+
def to_mermaid(argument, options) when Argument.is_from_input(argument) do
29+
target_id =
30+
options
31+
|> Keyword.fetch!(:parent_step)
32+
|> Map.fetch!(:name)
33+
|> mermaid_id(:step)
34+
35+
source_id =
36+
argument.source.name
37+
|> mermaid_id(:input)
38+
39+
source_id
40+
|> do_argument_link(target_id, argument, options)
41+
|> indentify(options)
42+
end
43+
44+
def to_mermaid(argument, options) do
45+
target_id =
46+
options
47+
|> Keyword.fetch!(:parent_step)
48+
|> Map.fetch!(:name)
49+
|> mermaid_id(:step)
50+
51+
source_id =
52+
argument.source.name
53+
|> mermaid_id(:step)
54+
55+
source_id
56+
|> do_argument_link(target_id, argument, options)
57+
|> indentify(options)
58+
end
59+
60+
defp do_argument_link(source_id, target_id, argument, options) do
61+
if options[:describe?] && is_binary(argument.description) do
62+
"#{source_id} -->|#{name(argument.name)} -- #{argument.description}|#{target_id}\n"
63+
else
64+
"#{source_id} -->|#{name(argument.name)}|#{target_id}\n"
65+
end
66+
end
67+
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
defmodule Reactor.Mermaid.Render.Reactor do
2+
@moduledoc false
3+
import Reactor.Mermaid.Utils
4+
5+
@doc false
6+
def to_mermaid(reactor, options) do
7+
inputs = emit_inputs(reactor, indent(options))
8+
steps = emit_steps(reactor, indent(options))
9+
10+
[
11+
indentify("subgraph #{inspect(reactor.id)}\n", options),
12+
inputs,
13+
steps,
14+
indentify("end", options)
15+
]
16+
end
17+
18+
defp emit_inputs(reactor, options) do
19+
Enum.map(reactor.inputs, fn input_name ->
20+
emit_input(reactor, input_name, options)
21+
end)
22+
end
23+
24+
defp emit_input(reactor, input_name, options) do
25+
if options[:describe?] do
26+
indentify(
27+
[
28+
"#{mermaid_id(input_name, :input)}>\"`",
29+
"**Input #{input_name}**\n",
30+
md_escape(Map.get(reactor.input_descriptions, input_name)),
31+
"`\"]\n"
32+
],
33+
options
34+
)
35+
else
36+
indentify(["#{mermaid_id(input_name, :input)}>\"Input #{input_name}\"]\n"], options)
37+
end
38+
end
39+
40+
defp emit_steps(reactor, options) do
41+
reactor.plan
42+
|> Graph.vertices()
43+
|> Enum.map(&Reactor.Step.to_mermaid(&1, options))
44+
end
45+
end

lib/reactor/mermaid/render/step.ex

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
defmodule Reactor.Mermaid.Render.Step do
2+
@moduledoc false
3+
import Reactor.Mermaid.Utils
4+
alias Reactor.Mermaid.Render
5+
6+
@doc false
7+
def to_mermaid(step, options) do
8+
module = impl_for(step)
9+
arg_options = Keyword.put(options, :parent_step, step)
10+
arguments = Enum.map(step.arguments, &Render.to_mermaid(&1, arg_options))
11+
12+
step =
13+
if Spark.implements_behaviour?(module, Render) do
14+
module.to_mermaid(step, options)
15+
else
16+
default_describe_step(step, module, options)
17+
end
18+
19+
[arguments, step]
20+
end
21+
22+
@doc false
23+
def default_describe_step(step, module, options) do
24+
if options[:describe?] do
25+
indentify(
26+
[
27+
mermaid_id(step.name, :step),
28+
"[\"`**",
29+
md_escape("#{name(step.name)}(#{inspect(module)})"),
30+
"**\n",
31+
md_escape(step.description),
32+
"`\"]\n"
33+
],
34+
options
35+
)
36+
else
37+
indentify(
38+
[
39+
mermaid_id(step.name, :step),
40+
"[\"",
41+
name(step.name),
42+
"(",
43+
inspect(module),
44+
")\"]\n"
45+
],
46+
options
47+
)
48+
end
49+
end
50+
51+
defp impl_for(%{impl: {module, _}}) when is_atom(module), do: module
52+
defp impl_for(%{impl: module}) when is_atom(module), do: module
53+
end

0 commit comments

Comments
 (0)