Current section
Files
Jump to
Current section
Files
lib/machinery_display.ex
defmodule MachineryDisplay do
@moduledoc """
Provides high-level ways to generate state machine graphics.
See also the `machinery_display` task to generate from a CLI.
"""
alias MachineryDisplay.Dot
@doc """
Handles generating outputs for every Machinery state machine encountered
in the system.
"""
@spec generate_all_outputs(atom()) :: [] | {:error, :no_state_machines_found}
def generate_all_outputs(application_name) when is_atom(application_name) do
case :application.get_key(application_name, :modules) do
{:ok, modules} ->
modules
|> Enum.filter(fn x ->
x.__info__(:functions) |> Enum.member?({:_machinery_states, 0})
end)
|> Enum.flat_map(&generate_for_module/1)
_ ->
{:error, :no_state_machines_found}
end
end
@doc """
Handles generating an output for a single state machine.
"""
@spec generate_for_module(module :: module()) :: :ok | {:error, atom(), map()}
def generate_for_module(module) do
module
|> generate_outputs()
|> Enum.map(fn {type, output} ->
with file_extension <- file_extension_for_output_type(type),
file_name <- "#{module.__info__(:module)}.#{file_extension}",
{:ok, file_path} <- write_to_file(file_name, output),
:ok <- compile_file(type, file_path) do
:ok
end
end)
end
#####################
# Private Functions #
#####################
@spec generate_outputs(state_machine :: module()) :: map()
defp generate_outputs(state_machine) do
# If other output types (different from :dot) exist,
# this is where you'd add them.
%{dot: Dot.create_output(state_machine)}
end
@spec write_to_file(file_name :: String.t(), output :: String.t()) ::
{:ok, Path.t()} | {:error, :compilation_error, map()}
defp write_to_file(file_name, output) do
root_dir = File.cwd!()
output_file = Path.join(root_dir, file_name)
case File.write(output_file, output) do
:ok ->
{:ok, output_file}
error ->
{:error, :compilation_error, %{output_file: output_file, output: output, error: error}}
end
end
@spec compile_file(:dot | atom(), file_name :: String.t()) ::
:ok | {:error, :compilation_error, map()}
defp compile_file(:dot, file_name) do
case System.cmd("dot", ["-Tpng", file_name, "-o", String.replace(file_name, ".dot", ".png")]) do
{"", 0} -> :ok
{msg, retval} -> {:error, :compilation_error, %{error_message: msg, retval: retval}}
end
end
defp compile_file(_type, _file_name) do
{:error, :unsupported_file_type}
end
@spec file_extension_for_output_type(type :: atom()) :: String.t() | {:error, :invalid_type}
defp file_extension_for_output_type(type) do
case type do
:dot -> Dot.file_extension()
_ -> {:error, :invalid_type}
end
end
end