defmodule Grains do @moduledoc """ Grains describes data flow as a graph with interchangeable parts. To accomplish that grains divides the graph (`Recipe`) from the implementation of the nodes (`Grains`). Together these parts can be used to make a `Bread` which describes how the graph translates to processes. Each process has a symbolic short name, which is resolved to the registered name of the process via a map (`%Bread{}.process_map`). """ alias Grains.Bread alias Grains.Recipe alias Grains.Supervisor defstruct [:map] @doc """ This function describes the default implementations for auxiliary grains, for example the periodic timer. Currently these default grains are provided: iex> Grains.default_grains() %{periodic: Grains.Timer} """ def default_grains() do %{ periodic: Grains.Timer } end def new(map) do %__MODULE__{map: map} end @doc """ Merges two grain maps into one. The function works the same as `Map.merge/2`: all grains in the second argument are added to the first, overriding any existing one. """ def merge(%__MODULE__{map: a}, %__MODULE__{map: b}) do a |> Map.merge(b) |> new() end defdelegate new_recipe(name, new), to: Grains.Recipe, as: :new defdelegate merge_recipes(name, a, b), to: Grains.Recipe, as: :merge defdelegate start_supervised(recipe, grains, args \\ []), to: Grains.Supervisor, as: :start_link @spec make_name(Recipe.t, atom, atom) :: atom def make_name(recipe = %Recipe{}, bread_id, short_name) do [recipe.name, bread_id, short_name] |> Module.concat() end @spec make_bread_name(Recipe.t, atom()) :: atom() def make_bread_name(recipe = %Recipe{}, bread_id) do [recipe.name, bread_id] |> Module.concat() end @spec get_name(Supervisor.t, atom) :: atom() def get_name(supervisor, short_name) do Module.concat([Supervisor.get_root_name(supervisor), short_name]) end @spec concat_name(Bread.t, atom) :: atom def concat_name(bread = %Bread{}, grain) do Module.concat([bread.name, grain]) end @doc """ Retrieve a grain's substate. This is similar to `:sys.get_state/1`. Note that this function should usually not be used, but it can be useful for debugging or testing. ## Errors The function exits the caller on error. For example, if no bread with the supplied id can be found, this results in a `no_proc` exit. """ @spec get_substate(atom) :: term() def get_substate(bread_id) do bread_id |> :sys.get_state() |> Map.get(:substate) end @doc """ Description of a periodic grain. """ def periodic(grain, period, opts \\ %{with_timestamps: false}) do {:periodic, grain, Map.put(opts, :period, period)} end @doc """ Combine two sets of recipes and grains. """ def combine({ra = %Recipe{}, ga = %Grains{}}, {rb = %Recipe{}, gb = %Grains{}}) do rab = Recipe.merge(ra.name, ra, rb) gab = Grains.merge(ga, gb) {rab, gab} end end