Packages
altworx_runbox
21.0.0
25.0.0
24.0.0
23.1.0
23.0.0
22.2.0
22.1.0
22.0.0
21.2.0
21.1.2
21.1.1
21.1.0
21.0.0
20.0.0
19.0.0
18.0.0
17.2.0
17.1.0
17.0.1
17.0.0
16.2.0
16.1.0
16.0.0
15.0.0
14.1.0
14.0.1
14.0.0
13.0.3
13.0.2
13.0.1
13.0.0
12.1.0
12.0.0
11.0.1
11.0.0
10.0.0
9.0.0
8.0.0
7.0.1
7.0.0
6.0.0
5.0.0
4.0.0
3.0.0
2.1.0
2.0.0
1.4.1
1.4.0
1.3.0
1.2.0
1.1.0
1.0.0
0.1.3
0.1.2
0.1.1
0.1.0
Runbox is a library for running Altworx scenarios.
Current section
Files
Jump to
Current section
Files
lib/runbox/scenario/notification.ex
defmodule Runbox.Scenario.Notification do
@moduledoc group: :internal
@moduledoc """
Module contains functions for working with scenario notifications.
"""
@notification_subfolder "notifications"
@overrides_subfolder "./settings/scenario_notification_overrides"
@template_subfolder "templates"
@type template() :: %{
notification_type: String.t(),
template_type: String.t(),
channel: String.t(),
language: String.t(),
filename: String.t(),
defined_in: :scenario | :deployment,
absolute_path: Path.t()
}
@doc """
Lists all template files defined for a particular scenario.
It returns both templates from the scenario and overrides from the deployment. The source can be
discriminated using the `:defined_in` key.
"""
@spec list_templates(String.t()) :: [template()]
def list_templates(scenario_id) do
default_subfolder = default_templates_base_folder(scenario_id)
override_subfolder = overrides_base_folder(scenario_id)
sources = [scenario: default_subfolder, deployment: override_subfolder]
for {source, base_folder} <- sources,
absolute_path <- Path.wildcard("#{base_folder}/*/*/*/*/*.eex") do
relative_path = Path.relative_to(absolute_path, base_folder)
[notification_type, template_type, channel, language, filename] = Path.split(relative_path)
%{
notification_type: notification_type,
template_type: template_type,
channel: channel,
language: language,
filename: filename,
defined_in: source,
absolute_path: absolute_path
}
end
end
@doc """
Lists all notification types for a scenario.
"""
@spec list_notification_types(String.t()) :: {:ok, [String.t()]}
def list_notification_types(scenario_id) do
default_subfolder = default_templates_base_folder(scenario_id)
override_subfolder = overrides_base_folder(scenario_id)
get(default_subfolder, override_subfolder)
end
@doc """
Lists all template types of the specified notification.
"""
@spec list_template_types(String.t(), String.t()) :: {:ok, [String.t()]}
def list_template_types(scenario_id, notification_type) do
default_subfolder =
Path.join([default_templates_base_folder(scenario_id), to_string(notification_type)])
override_subfolder =
Path.join([overrides_base_folder(scenario_id), to_string(notification_type)])
get(default_subfolder, override_subfolder)
end
@doc """
Lists all channels with templates defined for scenario, notification type and template type.
"""
@spec list_channels(String.t(), String.t(), String.t()) :: {:ok, [String.t()]}
def list_channels(scenario_id, notification_type, template_type) do
default_subfolder =
Path.join([
default_templates_base_folder(scenario_id),
to_string(notification_type),
to_string(template_type)
])
override_subfolder =
Path.join([
overrides_base_folder(scenario_id),
to_string(notification_type),
to_string(template_type)
])
get(default_subfolder, override_subfolder)
end
@doc """
Lists all template languages defined for scenario, notification type, template type and channel.
"""
@spec list_languages(String.t(), String.t(), String.t(), String.t()) :: {:ok, [String.t()]}
def list_languages(scenario_id, notification_type, template_type, channel) do
default_subfolder =
Path.join([
default_templates_base_folder(scenario_id),
to_string(notification_type),
to_string(template_type),
to_string(channel)
])
override_subfolder =
Path.join([
overrides_base_folder(scenario_id),
to_string(notification_type),
to_string(template_type),
to_string(channel)
])
get(default_subfolder, override_subfolder)
end
@doc """
Loads and evaluates templates specified by scenario, notification type, template type, channel
and language.
If language is not defined or does not exist, `fallback_language` is used.
"""
@spec load_and_eval_templates(
scenario_id :: String.t(),
notification_type :: String.t(),
template_type :: String.t(),
channel :: String.t(),
language :: String.t(),
data :: keyword(),
fallback_language :: String.t()
) :: {:ok, %{String.t() => String.t()}} | {:error, atom(), String.t()}
def load_and_eval_templates(
scenario,
notification_type,
template_type,
channel,
language,
data,
fallback_language
) do
with {:ok, templates} <-
load_templates(
scenario,
notification_type,
template_type,
channel,
language,
fallback_language
) do
{:ok, Enum.into(templates, %{}, fn {name, fun} -> {name, fun.(data)} end)}
end
end
defp load_templates(
scenario,
notification_type,
template_type,
channel,
language,
fallback_language
) do
overrides_folder =
Path.join([
overrides_base_folder(scenario),
template_subfolder(notification_type, template_type, channel, language)
])
overrides_fallback_folder =
if fallback_language do
Path.join([
overrides_base_folder(scenario),
template_subfolder(notification_type, template_type, channel, fallback_language)
])
end
default_folder =
Path.join([
default_templates_base_folder(scenario),
template_subfolder(notification_type, template_type, channel, language)
])
default_fallback_folder =
if fallback_language do
Path.join([
default_templates_base_folder(scenario),
template_subfolder(notification_type, template_type, channel, fallback_language)
])
end
[overrides_folder, overrides_fallback_folder, default_folder, default_fallback_folder]
|> Enum.reject(&is_nil/1)
|> Enum.reduce_while(
nil,
fn folder, _ ->
case compile_templates(folder) do
{:ok, _template_map} = res ->
{:halt, res}
{:error, _, _} = error ->
{:cont, error}
end
end
)
end
# finds all template files in the `folder` and compiles them.
# if any template fails then the whole compilation fails; also fails if no templates are found
defp compile_templates(folder) do
{templates, errors} =
folder
|> ls()
|> Enum.filter(&String.ends_with?(&1, ".eex"))
|> Enum.map(fn file ->
{template_name(file), compile_template(Path.join([folder, file]))}
end)
|> Enum.split_with(&match?({_, {:ok, _}}, &1))
case {templates, errors} do
{[], []} -> {:error, :no_templates_found, folder}
{_, []} -> {:ok, Enum.into(templates, %{}, fn {name, {:ok, fun}} -> {name, fun} end)}
{_, [{_name, error} | _rest]} -> error
end
end
defp compile_template(file) do
compiled = EEx.compile_file(file)
{:ok, fn bindings -> elem(Code.eval_quoted(compiled, bindings, file: file), 0) end}
rescue
_ in File.Error -> {:error, :not_found, file}
_ in EEx.SyntaxError -> {:error, :bad_syntax, file}
end
defp default_templates_base_folder(scenario_id) do
Path.join(scenario_notifications_path(scenario_id), @template_subfolder)
end
defp overrides_base_folder(scenario_id) do
path = Runbox.Utils.Path.resolve_path(@overrides_subfolder)
Path.join([path, scenario_id, @template_subfolder])
end
defp template_subfolder(notification_type, template_type, channel, language) do
Path.join([
to_string(notification_type),
to_string(template_type),
to_string(channel),
to_string(language)
])
end
defp scenario_notifications_path(scenario_id) do
scenario_app = Application.fetch_env!(:runbox, :scenario_app)
Path.join([:code.priv_dir(scenario_app), @notification_subfolder, to_string(scenario_id)])
end
# lists all files and dirs in a path
defp ls(path) do
case File.ls(path) do
{:ok, files} -> files
_ -> []
end
end
# extract template name from filename (i.e. "content.eex")
defp template_name(file) do
String.slice(file, 0..-5//1)
end
defp get(default_subfolder, override_subfolder) do
defaults = ls(default_subfolder)
overrides = ls(override_subfolder)
{:ok,
defaults
|> Enum.concat(overrides)
|> Enum.uniq()}
end
end