defmodule ChildsPlay do
@moduledoc """
Make building a list of children a `Supervisor` manages a problem of the
past.
We've all been there before. Your new project starts out with a clean
Supervision tree of only a few children. Make sure this is running, make sure
that is running. Life is good. But eventually rules start to pop up that make
it so the list of children you start gets more and more complex.
Some of the greatest hits of the projects I've been involved in.
> "Don't start the email server in Dev. We use a mocked version there."
> "If its a cronjob node we don't want to start up consumers."
This leads to all sorts of variations on how to filter and tranform the list
of children for our Supervisors. Most lead to bloated descriptions and
confusing logic. We can turn it into childs play....
```elixir
defmodule MyApp.Application do
use Application
# importing not required - just cleaner
import ChildsPlay
def start(_type, _args) do
[
# Add regular children you know and love (possibly more than the others)
MyApp.Repo,
given(
Application.get_env(:my_app, :cronjob_node?, false),
ConsumerSupervisor
),
given(
System.get_env("ENV") == "PROD",
[
MyApp.MailServer,
MyApp.MetricsServer
]
),
]
|> build()
|> Supervisor.start_link(strategy: :one_for_one)
end
end
```
## Building child lists
The core function at work is `given/2` which takes a boolean (or a predicate
function) and a single child or a list of children to start. When the value is
true the children will be rendered.
The core data structure at play here is just a list, the trick that
`ChildsPlay` adds is that now nested lists get flattened. What this means is
that we could go as far as doing this if we wanted to.
```elixir
defmodule MyApp.Application do
use Application
import ChildsPlay
def start(_type, _args) do
[
given(
System.get_env("ENV") == "PROD",
[
MyApp.MailServer,
given(
Application.get_env(:my_app, :cronjob_node?, false),
[
MyApp.Consumer1,
MyApp.Consumer2
]
)
]
),
]
|> build()
|> Supervisor.start_link(strategy: :one_for_one)
end
end
```
With great power comes great responsibility. Always favor whatever is more
declarative and childs play for all!
"""
@type predicate :: (() -> boolean())
@type children :: [
[]
| Supervisor.child_spec()
| {module(), term()}
| module()
| (old_erlang_child_spec :: :supervisor.child_spec())
]
@spec given(nil | boolean() | predicate(), children()) :: children()
@doc """
If `condition` is true, or predicate returning true allow children.
```elixir
[
ChildsPlay.given(true, Agent),
ChildsPlay.given(false, Agent),
ChildsPlay.given(true, [Worker1, Worker2]),
ChildsPlay.given(fn -> false end, [Worker3, Worker4])
]
|> ChildsPlay.build()
|> Supervisor.start_link(strategy: :one_for_one)
```
"""
def given(condition, children) when is_function(condition) do
if condition.() do
children
else
[]
end
end
def given(true, children), do: children
def given(false, _children), do: []
def given(nil, _children), do: []
@spec build(children()) :: [any()]
def build(children) do
List.flatten(children)
end
end