lib/runbox/notifications/template_helper.ex

defmodule Runbox.Notifications.TemplateHelper do
  @moduledoc group: :utilities
  @moduledoc """
  Set of utility functions for use in notification templates.
  """

  alias Timex.Format.DateTime.Formatter
  alias Timex.Timezone

  @doc """
  Formats timestamp into the given format in the given timezone.
  """
  def format_ts(ts, format_str, target_tz \\ "Etc/UTC")

  def format_ts(nil, _, _) do
    nil
  end

  def format_ts(ts, format_str, target_tz) do
    ts
    |> DateTime.from_unix!(:millisecond)
    |> Timezone.convert(target_tz)
    |> Formatter.format!(format_str)
  end

  @doc """
  Returns the UI url.
  """
  def ui_url do
    Application.fetch_env!(:runbox, :ui_url)
  end

  @doc """
  Returns a URL to the asset detail.

  The type of UI can be specified, and the link is generated to work with that UI. Can be `:new` or
  `:old`.
  """
  @spec asset_link(String.t(), :new | :old) :: String.t()
  def asset_link(asset_id, ui_type \\ :new)

  def asset_link(asset_id, :new) do
    Path.join([
      ui_url(),
      "network",
      asset_id
    ])
  end

  def asset_link(asset_id, :old) do
    Path.join([
      ui_url(),
      "/assets/browser",
      asset_id
    ])
  end

  @doc """
  Formats a number by separating thousands with a separator.
  """
  def format_number(number, separator \\ " ") do
    number
    |> to_charlist()
    |> Enum.reverse()
    |> Enum.chunk_every(3)
    |> Enum.intersperse(separator)
    |> List.flatten()
    |> Enum.reverse()
    |> to_string()
  end

  @doc """
  Wraps the given EEx content with a EEx template.

  Templates can be bundled with a scenario or be a part of the system, `source`
  should be set to `scenario_name` for scenario-based
  templates and `:system` for templates bundled with the system.

  The templates that can be included, so called shared templates, are stored in
  `shared` folder
  (`scenarios/priv/notifications/{scenario_name}/shared`).
  Each shared template consists of two files - header and footer - which are
  wrapped around the given content. The filenames have `_header` and `_footer`
  appended, for `template_name = "email"` the two filenames are
  `email_header.eex` and `email_footer.eex`. System templates are stored in
  `apps/asset_map/templates/shared`.

  Via `context` you can pass additional parameters to the shared template
  (passed as assigns). The expected context depends on the template.

  ### Example usage
  ```
  <% alias Runbox.Notifications.TemplateHelper %>
  <% require TemplateHelper %>
  <%= TemplateHelper.include_template({"test", 1}, "email_rich", title: "Test email") do %>
    <h1>Test Email</h1>
    <p>This text is surrounded by the template...</p>
  <% end %>
  ```
  """
  defmacro include_template(source, template_name, context \\ [], macro_params)

  defmacro include_template(source, template_name, context, do: content) do
    case find_shared_templates(source, template_name) do
      {:ok, header, footer} ->
        quote bind_quoted: [header: header, footer: footer, content: content, context: context] do
          [
            EEx.eval_file(header, assigns: context),
            content,
            EEx.eval_file(footer, assigns: context)
          ]
        end

      _ ->
        quote do
          unquote(content)
        end
    end
  end

  defp find_shared_templates(:system, template_name) do
    [
      :code.priv_dir(:runbox),
      "templates/shared"
    ]
    |> Path.join()
    |> get_shared_template(template_name)
  end

  defp find_shared_templates(scenario_name, template_name) when is_binary(scenario_name) do
    [
      :code.priv_dir(Application.fetch_env!(:runbox, :scenario_app)),
      "notifications",
      scenario_name,
      "shared"
    ]
    |> Path.join()
    |> get_shared_template(template_name)
  end

  defp get_shared_template(base_path, template_name) do
    header = Path.join(base_path, "#{template_name}_header.eex")
    footer = Path.join(base_path, "#{template_name}_footer.eex")

    if File.exists?(header) && File.exists?(footer) do
      {:ok, header, footer}
    else
      :error
    end
  end
end