defmodule Mix.Tasks.Liberator.Chart do
@shortdoc "Generates source text for a chart of Liberator's decision tree"
@moduledoc """
Generates source text for a decision tree chart for a Liberator resource.
The chart is compatible with the [Graphviz](https://graphviz.org/) graph visualization software.
```sh
mix liberator.chart
```
By default, this function will print the default decision tree to standard output.
This task can also take a module argument for any module that `use`s `Liberator.Resource`,
in which case the decision tree for the given module will be printed.
```sh
mix liberator.chart MyApp.MyResource
```
You can also provide the `--output` or `-o` option to print the chart source to a file.
```sh
mix liberator.chart -o myresource.dot MyApp.MyResource
```
## Generating a chart with the returned source code
Unfortunately, there's not a Graphviz binding for Elixir.
If you want to create an actual image of your chart,
you will have to install [Graphviz](https://graphviz.org/),
or use one of its language bindings for another language.
Once you have installed Graphviz, you can run a command like the following to generate an image
```sh
dot myresource.dot -Tsvg -o myresource.svg
```
"""
use Mix.Task
def run(args) do
{opts, argv, _errors} =
OptionParser.parse(args, aliases: [o: :output], strict: [output: :string])
if length(argv) > 1 do
IO.puts(:stderr, "More than one module name given, ignoring all after the first")
end
base_module =
if Enum.empty?(argv) do
Liberator.Default.DecisionTree
else
"Elixir.#{List.first(argv)}"
|> String.to_existing_atom()
end
Code.ensure_loaded(base_module)
unless function_exported?(base_module, :decisions, 0) and
function_exported?(base_module, :actions, 0) and
function_exported?(base_module, :handlers, 0) do
raise "The given module, #{base_module}, does not implement " <>
"the required functions from Liberator.Resource. " <>
"Make sure that module has `use Liberator.Resource` in it."
end
chart = dot(base_module)
if filename = Keyword.get(opts, :output) do
File.write!(filename, chart)
IO.puts("Chart saved to #{filename}")
else
IO.puts(chart)
end
end
defp dot(base_module) do
handler_rank_group =
base_module.handlers()
|> Map.keys()
|> Enum.map(fn handler ->
~s("#{handler}")
end)
|> Enum.join(" ")
handler_shapes =
base_module.handlers()
|> Map.keys()
|> Enum.flat_map(fn handler ->
[
~s("#{handler}" [shape=box])
]
end)
|> Enum.join("\n")
decisions =
base_module.decisions()
|> Enum.flat_map(fn {decision_fn, {true_step, false_step}} ->
[
~s("#{decision_fn}" -> "#{true_step}" [label="yes"]),
~s("#{decision_fn}" -> "#{false_step}" [label="no"])
]
end)
|> Enum.join("\n")
actions =
base_module.actions()
|> Enum.flat_map(fn {action, after_action} ->
[
~s("#{action}" [shape=box]),
~s("#{action}" -> "#{after_action}")
]
end)
|> Enum.join("\n")
"""
strict digraph G {
{ rank=same #{handler_rank_group}}
#{handler_shapes}
#{decisions}
#{actions}
}
"""
end
end