Current section
Files
Jump to
Current section
Files
lib/grains/recipe.ex
defmodule Grains.Recipe do
@moduledoc """
A Recipe describes the data flow between the processes.
"""
defstruct [:map, :name]
@type t :: %__MODULE__{
name: atom,
map: map
}
def new(name, map) do
%__MODULE__{
name: name,
map: map
}
end
@doc """
Merges to recipes by combining all edges.
"""
def merge(new_name, %__MODULE__{map: a}, %__MODULE__{map: b}) do
ab = Map.merge(a, b, fn _k, v1, v2 -> Enum.uniq(List.wrap(v1) ++ List.wrap(v2)) end)
new(new_name, ab)
end
@doc """
Returns a mermaid flowchart as a string.
## Arguments
`to_mermaid/2` supports an optional `args` argument. The following
options are supported:
* `type`: Graph type as string, can be either `"flowchart"` or `"graph"` (default: `"graph"`)
* `direction`: Flowchart orientation (default: `"TD"`)
* `merge_edges`: Merge multiple edges between a predecessor and a successor (default: `false`)
"""
def to_mermaid(recipe, args \\ []) do
defaults = %{type: "graph", direction: "TD", merge_edges: false}
args = Map.merge(defaults, Map.new(args))
header = "#{args.type} #{args.direction}"
body =
recipe.map
|> Enum.flat_map(&unfold/1)
|> format_patterns(args)
|> Enum.map(&to_mermaid_edge/1)
Enum.join([header | body], " \n")
end
defp unfold({l, r}) when is_atom(r) do
[{l, r, []}]
end
defp unfold({l, rs}) when is_list(rs) do
Enum.flat_map(rs, fn r -> unfold({l, r}) end)
end
defp unfold({l, {:periodic, rs, p}}) do
rs
|> List.wrap()
|> Enum.flat_map(fn r -> unfold({l, r}) end)
|> Enum.map(fn {l, r, e} -> {l, r, [{:periodic, p} | e]} end)
end
defp unfold({l, {:route, _, rs, s}}) when is_list(rs) do
rs
|> Enum.flat_map(fn r -> unfold({l, r}) end)
|> Enum.map(fn {l, r, e} -> {l, r, [{:route, s} | e]} end)
end
defp unfold({l, {:ghost, rs, args}}) do
rs
|> List.wrap()
|> Enum.flat_map(fn r -> unfold({l, r}) end)
|> Enum.map(fn {l, r, e} -> {l, r, [{:ghost, args} | e]} end)
end
defp format_patterns(edges, %{merge_edges: false}) do
Enum.map(edges, fn {from, to, pattern} -> {from, to, [pattern]} end)
end
defp format_patterns(edges, %{merge_edges: true}) do
edges
|> Enum.reduce(%{}, fn {from, to, pattern}, acc ->
update_in(acc, [{from, to}], &update_pattern(&1, pattern))
end)
|> Enum.map(fn {{from, to}, patterns} -> {from, to, patterns} end)
end
defp update_pattern(nil, pattern), do: [pattern]
defp update_pattern(previous, pattern), do: [pattern | previous]
defp to_mermaid_edge({l, r, edge}) do
edge_type = to_edge_type(edge)
"#{to_mermaid_node(l)} #{edge_type}#{to_mermaid_edge_att(edge)} #{to_mermaid_node(r)}"
end
defp to_mermaid_edge_att(l) do
case Enum.map_join(l, "<br />", &to_attr/1) do
"" -> ""
non_empty -> "|\"#{non_empty}\"|"
end
end
defp to_attr(l) when is_list(l), do: Enum.map_join(l, ", ", &to_attr/1)
defp to_attr({:route, s}), do: "#{s}"
defp to_attr({:periodic, _}), do: "periodic"
defp to_attr({:ghost, description: nil}), do: ""
defp to_attr({:ghost, description: desc}), do: desc
defp to_attr(other), do: other
defp to_edge_type(edge) when is_list(edge) do
if edge |> Enum.concat() |> Enum.any?(&ghost?/1) do
"-.->"
else
"-->"
end
end
defp ghost?({:ghost, _}), do: true
defp ghost?(_other), do: false
defp to_mermaid_node(atom) do
atom |> Atom.to_string() |> String.replace_leading("Elixir.", "")
end
end