Current section

Files

Jump to
grains lib grains bread.ex
Raw

lib/grains/bread.ex

defmodule Grains.Bread do
@moduledoc """
A %Bread{} is the compiled version of Recipe plus Grains
that allow the Supervisor to
run the processes and wire them correctly.
"""
alias Grains.Recipe
@type t :: %__MODULE__{}
defstruct [
:id,
:original_recipe,
:original_grains,
:child_specs,
:final_recipe,
:sup,
:name,
:final_grains,
:process_map,
:default_grains
]
@doc """
Takes a recipe, grains and optionally a map of default implementations
and returns a bread. Does not start any processes yet.
## Optional Arguments
* `:default_grains` Replace the default grains
* `:id` Set a custom bread id
"""
def bake(recipe = %Recipe{}, grains = %Grains{}, args \\ []) do
default_grains = args[:default_grains] || Grains.default_grains()
id = args[:id] || gen_id()
builtin_grains = setup_builtin_grains(recipe, default_grains)
final_grains = %Grains{grains | map: Map.merge(grains.map, builtin_grains)}
process_map = process_map(final_grains, recipe, id)
name = Grains.make_bread_name(recipe, id)
prebread = %__MODULE__{
id: id,
original_recipe: recipe,
original_grains: grains,
final_grains: final_grains,
process_map: process_map,
default_grains: default_grains,
name: name
}
build_aux_edges(prebread)
|> child_specs()
end
# Builds a mapping from short names to process names.
defp process_map(grains, recipe, bread_id) do
Enum.map(grains.map, fn {short_name, _} ->
process_name = Grains.make_name(recipe, bread_id, short_name)
{short_name, process_name}
end)
|> Enum.into(%{})
end
@doc """
Generates a random atom to be used as an id for the Bread
"""
def gen_id do
8
|> :crypto.strong_rand_bytes()
|> Base.encode32(padding: false)
|> String.to_atom()
end
defp child_specs(bread = %__MODULE__{final_grains: grains, process_map: process_map}) do
Enum.map(grains.map, fn {short_name, {module, args, opts}} ->
process_name = Map.fetch!(process_map, short_name)
%{
id: process_name,
start:
{Grains.GenGrain, :start_link,
[module, bread, short_name, args, [name: process_name || opts]]}
}
end)
|> (&%__MODULE__{bread | child_specs: &1}).()
end
# Function takes a recipe and a map of the default implemations,
# to setup the grains that are needed for build in
# functionality, like the periodic timer.
defp setup_builtin_grains(recipe, default_grains) do
recipe.map
|> Enum.flat_map(fn
{left, right} when is_list(right) ->
Enum.map(right, &{left, &1})
{left, right} ->
[{left, right}]
end)
|> Enum.filter(fn
{_, {:periodic, _, _}} -> true
_ -> false
end)
|> Enum.map(fn {left, {:periodic, grain, opts}} ->
impl = Map.fetch!(default_grains, :periodic)
short_name = impl.name(left, grain)
{short_name, {impl, opts, []}}
end)
|> Enum.into(%{})
end
# Function takes a half done (pre)-bread to
# fill in the edges in the graph that are needed for build in
# functionality, like the periodic timer.
defp build_aux_edges(prebread) do
final_recipe =
Enum.flat_map(
prebread.original_recipe.map,
&build_aux_edges1(&1, prebread.default_grains)
)
%__MODULE__{prebread | final_recipe: final_recipe}
end
defp build_aux_edges1({k, v}, default_grains) do
case v do
{:periodic, grain, _opts} ->
impl = Map.fetch!(default_grains, :periodic)
tname = impl.name(k, grain)
[{k, tname}, {tname, grain}]
l when is_list(l) ->
Enum.flat_map(l, &build_aux_edges1({k, &1}, default_grains))
v when is_atom(v) ->
[{k, v}]
end
end
def check(%__MODULE__{process_map: processes_map, final_recipe: final_recipe}) do
grain_short_names = Map.keys(processes_map)
recipe_names =
final_recipe
|> Enum.flat_map(&Tuple.to_list/1)
case Enum.reject(recipe_names, &Enum.member?(grain_short_names, &1)) do
[] ->
:ok
gs ->
{:error, {:missing_grain, gs}}
end
end
end