defmodule Runbox.Scenario.Notification do
@moduledoc """
Module contains functions for working with scenario notifications.
"""
@spec_path Path.join(["spec.exs"])
@doc """
Tries to load the notification spec of the given scenario.
Returns
* `{:ok, notification_spec}` if the spec is found
* `{:error, :cannot_load, exception}` if the spec file cannot be found
* `{:error, :bad_syntax, exception}` if the spec file was found but contains syntactic errors
"""
@spec load_spec(String.t()) :: {:ok, any} | {:error, atom(), any}
def load_spec(scenario_id) do
spec_path = Path.join(scenario_notifications_path(scenario_id), @spec_path)
try do
{specs, _} = Code.eval_file(spec_path)
{:ok, specs}
rescue
e in Code.LoadError -> {:error, :cannot_load, e}
e in SyntaxError -> {:error, :bad_syntax, e}
end
end
@doc """
Lists all notification types for a scenario.
"""
@spec list_notification_types(String.t()) :: {:ok, [String.t()]} | {:error, File.posix()}
def list_notification_types(scenario_id) do
scenario_id
|> template_path()
|> ls()
end
@doc """
Lists all template types of the specified notification.
"""
@spec list_template_types(String.t(), String.t()) :: {:ok, [String.t()]} | {:error, File.posix()}
def list_template_types(scenario_id, notification_type) do
[template_path(scenario_id), to_string(notification_type)]
|> Path.join()
|> ls()
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()]} | {:error, File.posix()}
def list_channels(scenario_id, notification_type, template_type) do
[template_path(scenario_id), to_string(notification_type), to_string(template_type)]
|> Path.join()
|> ls()
end
@doc """
Lists all template languages defined for senario, notification type, template type and channel.
"""
@spec list_languages(String.t(), String.t(), String.t(), String.t()) ::
{:ok, [String.t()]} | {:error, File.posix()}
def list_languages(scenario_id, notification_type, template_type, channel) do
[
template_path(scenario_id),
to_string(notification_type),
to_string(template_type),
to_string(channel)
]
|> Path.join()
|> ls()
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
folder =
Path.join([
template_path(scenario),
to_string(notification_type),
to_string(template_type),
to_string(channel),
to_string(language)
])
fallback_folder =
if fallback_language do
Path.join([
template_path(scenario),
to_string(notification_type),
to_string(template_type),
to_string(channel),
to_string(fallback_language)
])
end
case compile_templates(folder) do
{:ok, _template_map} = res ->
res
{:error, _, _} = error ->
if fallback_folder do
compile_templates(fallback_folder)
else
error
end
end
end
# finds all template files in the `folder` and compiles them.
# if any template failes then the whole compile fails, also fails if no templates are found
defp compile_templates(folder) do
{templates, errors} =
folder
|> ls_if_exists()
|> 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 template_path(scenario_id) do
Path.join(scenario_notifications_path(scenario_id), "templates")
end
defp scenario_notifications_path(scenario_id) do
scenario_app = Application.fetch_env!(:runbox, :scenario_app)
Path.join([:code.priv_dir(scenario_app), "notifications", to_string(scenario_id)])
end
# lists all files and dirs in a path
defp ls(path) do
if File.dir?(path) do
File.ls(path)
else
{:ok, []}
end
end
defp ls_if_exists(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)
end
end