Skip to content

Commit 4dffc97

Browse files
committed
wip: we have a pretty nice working model now and started building out custom flowcharts for steps with magic effects.
1 parent eb90c16 commit 4dffc97

File tree

17 files changed

+647
-277
lines changed

17 files changed

+647
-277
lines changed

lib/reactor.ex

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

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

5453
@type context :: %{optional(atom) => any}
5554
@type context_arg :: Enumerable.t({atom, any})
@@ -220,8 +219,4 @@ defmodule Reactor do
220219
{:error, reason} -> raise reason
221220
end
222221
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)
227222
end

lib/reactor/argument.ex

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

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

8-
@behaviour Reactor.Mermaid.Render
9-
108
alias Reactor.{Argument, Template}
119
import Reactor.Template, only: :macros
1210

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

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

lib/reactor/mermaid.ex

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
defmodule Reactor.Mermaid do
22
@moduledoc """
33
Converts Reactors and their related entities into a Mermaid diagram.
4+
5+
See [Mermaid](https://mermaid.js.org/) for more information.
46
"""
7+
58
@options Spark.Options.new!(
69
expand?: [
710
type: :boolean,
@@ -18,20 +21,26 @@ defmodule Reactor.Mermaid do
1821
direction: [
1922
type: {:in, [:top_to_bottom, :bottom_to_top, :right_to_left, :left_to_right]},
2023
required: false,
21-
default: :top_to_bottom,
24+
default: :left_to_right,
2225
doc: "The direction to render the flowchart"
2326
],
2427
indent: [
2528
type: :non_neg_integer,
2629
required: false,
2730
default: 0,
2831
doc: "How much to indent the resulting mermaid"
32+
],
33+
output: [
34+
type: {:in, [:iodata, :binary]},
35+
required: false,
36+
default: :iodata,
37+
doc: "Specify the output format. `iodata` is more performant"
2938
]
3039
)
3140

3241
@type options :: unquote(Spark.Options.option_typespec(@options))
3342

34-
import __MODULE__.Utils
43+
@callback to_mermaid(module | struct, options) :: {:ok, Node.t()} | {:error, any}
3544

3645
@doc """
3746
Convert the Reactor into Mermaid.
@@ -73,14 +82,40 @@ defmodule Reactor.Mermaid do
7382
end
7483

7584
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)}
85+
with {:ok, reactor} <- Reactor.Planner.plan(reactor),
86+
{:ok, sub_graph} <- __MODULE__.Reactor.to_mermaid(reactor, options),
87+
{:ok, root} <- root_node(sub_graph, options) do
88+
iodata = __MODULE__.Node.render(root, options[:indent] || 0)
89+
90+
if options[:output] == :binary do
91+
string =
92+
iodata
93+
|> IO.iodata_to_binary()
94+
|> String.trim_trailing(" ")
95+
96+
{:ok, string}
97+
else
98+
{:ok, iodata}
99+
end
79100
end
80101
end
81102

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"
103+
defp root_node(sub_graph, options) do
104+
{:ok,
105+
%__MODULE__.Node{
106+
id: "root",
107+
pre: [
108+
"flowchart ",
109+
__MODULE__.Utils.direction(options[:direction])
110+
],
111+
children: [
112+
"\n",
113+
"start{\"Start\"}\n",
114+
"start==>",
115+
sub_graph.id,
116+
"\n",
117+
sub_graph
118+
]
119+
}}
120+
end
86121
end
Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,62 @@
1-
defmodule Reactor.Mermaid.Render.Argument do
1+
defmodule Reactor.Mermaid.Argument do
22
@moduledoc false
33
import Reactor.Mermaid.Utils
4-
alias Reactor.Argument
4+
alias Reactor.{Argument, Mermaid.Node}
55
require Argument
6+
@behaviour Reactor.Mermaid
67

78
@doc false
89
def to_mermaid(argument, options) when Argument.is_from_value(argument) do
910
target_id =
1011
options
11-
|> Keyword.fetch!(:parent_step)
12-
|> Map.fetch!(:name)
13-
|> mermaid_id(:step)
12+
|> Keyword.fetch!(:target_id)
1413

1514
source_id =
1615
argument.source.value
1716
|> mermaid_id(:value)
1817

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-
)
18+
{:ok,
19+
%Node{
20+
pre: [
21+
source_id,
22+
"{{\"`",
23+
md_escape(inspect(argument.source.value, pretty: true)),
24+
"`\"}}",
25+
do_argument_link(source_id, target_id, argument, options)
26+
]
27+
}}
2628
end
2729

2830
def to_mermaid(argument, options) when Argument.is_from_input(argument) do
2931
target_id =
3032
options
31-
|> Keyword.fetch!(:parent_step)
32-
|> Map.fetch!(:name)
33-
|> mermaid_id(:step)
33+
|> Keyword.fetch!(:target_id)
3434

3535
source_id =
36-
argument.source.name
36+
{options[:reactor_id], argument.source.name}
3737
|> mermaid_id(:input)
3838

39-
source_id
40-
|> do_argument_link(target_id, argument, options)
41-
|> indentify(options)
39+
content =
40+
source_id
41+
|> do_argument_link(target_id, argument, options)
42+
43+
{:ok, %Node{pre: content}}
4244
end
4345

4446
def to_mermaid(argument, options) do
4547
target_id =
4648
options
47-
|> Keyword.fetch!(:parent_step)
48-
|> Map.fetch!(:name)
49-
|> mermaid_id(:step)
49+
|> Keyword.fetch!(:target_id)
5050

5151
source_id =
52-
argument.source.name
52+
{options[:reactor_id], argument.source.name}
5353
|> mermaid_id(:step)
5454

55-
source_id
56-
|> do_argument_link(target_id, argument, options)
57-
|> indentify(options)
55+
content =
56+
source_id
57+
|> do_argument_link(target_id, argument, options)
58+
59+
{:ok, %Node{pre: content}}
5860
end
5961

6062
defp do_argument_link(source_id, target_id, argument, options) do

lib/reactor/mermaid/node.ex

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
defmodule Reactor.Mermaid.Node do
2+
@moduledoc """
3+
A node in the mermaid graph
4+
"""
5+
defstruct children: [], id: nil, post: [], pre: []
6+
7+
@type node_data :: node_list | binary | byte | t
8+
@type node_list :: [node_data] | node_data
9+
10+
@type t :: %__MODULE__{
11+
children: node_list,
12+
id: String.t(),
13+
post: node_list,
14+
pre: node_list
15+
}
16+
17+
@indent " "
18+
19+
@doc """
20+
Render the node at the provided indent level.
21+
"""
22+
@spec render(t, non_neg_integer()) :: iodata
23+
def render(node, indent) do
24+
indent =
25+
[@indent]
26+
|> Stream.cycle()
27+
|> Enum.take(indent)
28+
29+
do_render([node], [], indent)
30+
end
31+
32+
defguardp is_byte(byte) when is_integer(byte) and byte >= 0 and byte <= 255
33+
34+
defp do_render([], result, _indent), do: result
35+
defp do_render([[] | tail], result, indent), do: do_render(tail, result, indent)
36+
37+
defp do_render([head | tail], result, indent) when is_list(head) do
38+
head = do_render(head, [], indent)
39+
do_render(tail, [result, head], indent)
40+
end
41+
42+
defp do_render([head | tail], result, indent) when is_struct(head, __MODULE__) do
43+
pre = do_render(head.pre, [], indent)
44+
children = do_render(head.children, [], [indent, @indent])
45+
post = do_render(head.post, [], indent)
46+
do_render(tail, [result, pre, children, post], indent)
47+
end
48+
49+
defp do_render([head | tail], result, indent) when is_binary(head),
50+
do: do_render(tail, [result, indent(head, indent)], indent)
51+
52+
defp do_render([head | tail], result, indent)
53+
when is_integer(head) and head >= 0 and head <= 255,
54+
do: do_render(tail, [result, indent(head, indent)], indent)
55+
56+
defp do_render(bin, result, indent) when is_binary(bin),
57+
do: do_render([], [result, indent(bin, indent)], indent)
58+
59+
defp do_render(int, result, indent) when is_byte(int),
60+
do: do_render([], [result, indent(int, indent)], indent)
61+
62+
defp indent("", _indent), do: ""
63+
64+
defp indent(input, []), do: input
65+
66+
defp indent(bin, indent) when is_binary(bin) do
67+
bin
68+
|> String.split("\n")
69+
|> Enum.intersperse(["\n", indent])
70+
end
71+
72+
defp indent(?\n, indent) do
73+
[?\n, indent]
74+
end
75+
76+
defp indent(int, _indent) when is_byte(int), do: IO.chardata_to_string([int])
77+
end

lib/reactor/mermaid/reactor.ex

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
defmodule Reactor.Mermaid.Reactor do
2+
@moduledoc false
3+
alias Reactor.Mermaid.{Node, Step}
4+
import Reactor.Utils
5+
import Reactor.Mermaid.Utils
6+
@behaviour Reactor.Mermaid
7+
8+
@doc false
9+
@impl true
10+
def to_mermaid(reactor, options) when is_struct(reactor, Reactor) do
11+
id = mermaid_id(reactor.id, :reactor)
12+
13+
with {:ok, reactor} <- Reactor.Planner.plan(reactor),
14+
{:ok, inputs} <- generate_inputs(reactor, options),
15+
{:ok, steps} <- generate_steps(reactor, options) do
16+
return_step_id = mermaid_id({reactor.id, reactor.return}, :step)
17+
return_id = mermaid_id(reactor.id, :return)
18+
19+
{:ok,
20+
%Node{
21+
id: id,
22+
pre: [
23+
"subgraph ",
24+
id,
25+
"[\"",
26+
name(reactor.id),
27+
"\"]"
28+
],
29+
children: [
30+
"\n",
31+
"direction ",
32+
direction(options[:direction]),
33+
"\n",
34+
inputs,
35+
steps,
36+
return_id,
37+
"{\"Return\"}\n",
38+
return_step_id,
39+
"==>",
40+
return_id
41+
],
42+
post: [
43+
"\n",
44+
"end\n"
45+
]
46+
}}
47+
end
48+
end
49+
50+
defp generate_inputs(reactor, options) do
51+
reactor.inputs
52+
|> map_while_ok(&generate_input(&1, reactor, options))
53+
end
54+
55+
defp generate_input(input_name, reactor, options) do
56+
id = mermaid_id({reactor.id, input_name}, :input)
57+
58+
content =
59+
if options[:describe?] do
60+
[
61+
id,
62+
">\"`",
63+
"**Input ",
64+
to_string(input_name),
65+
"**",
66+
"\n",
67+
md_escape(Map.get(reactor.input_descriptions, input_name)),
68+
"`\"]",
69+
"\n"
70+
]
71+
else
72+
[id, ">\"Input ", to_string(input_name), "\"]\n"]
73+
end
74+
75+
{:ok, %Node{id: id, pre: content}}
76+
end
77+
78+
defp generate_steps(reactor, options) do
79+
options = Keyword.put(options, :reactor_id, reactor.id)
80+
81+
reactor.plan
82+
|> Graph.vertices()
83+
|> reduce_while_ok([], fn step, nodes ->
84+
with {:ok, node} <- Step.to_mermaid(step, options) do
85+
{:ok, [node | nodes]}
86+
end
87+
end)
88+
end
89+
end

0 commit comments

Comments
 (0)