lib/database/helper.ex

defmodule MishkaInstaller.Database.Helper do
  @moduledoc """
  This module provides some functions as utility tools to work with a database and other things.
  """

  import Ecto.Changeset
  require Logger

  @doc """
  If you need to convert database errors into a list, this function can be helpful.
  One of its uses can be correcting returned errors from the database into a list and converting it into JSON.

  ## Examples

  ```elixir
  MishkaInstaller.Database.Helper.translate_errors(changeset)
  ```
  """
  @spec translate_errors(Ecto.Changeset.t()) :: %{optional(atom) => [binary | map]}
  def translate_errors(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
      Enum.reduce(opts, msg, fn {key, value}, acc ->
        String.replace(acc, "%{#{key}}", to_string(value))
      end)
    end)
  end

  @doc """
  Converting string map to atom map.

  ## Examples

  ```elixir
  MishkaInstaller.Database.Helper.convert_string_map_to_atom_map(%{"name" => "Mishka"})
  ```
  """
  @spec convert_string_map_to_atom_map(map) :: map
  def convert_string_map_to_atom_map(map) do
    map
    |> Map.new(fn {k, v} ->
      {String.to_existing_atom(k), v}
    end)
  end

  @doc """
  UUID validation for ecto schema.

  ## Examples

  ```elixir
  MishkaInstaller.Database.Helper.validate_binary_id(12)
  # OR
  MishkaInstaller.Database.Helper.validate_binary_id("8c512ac2-e002-4589-a93f-b479e46c249d")
  ```
  """
  @spec validate_binary_id(Ecto.Changeset.t(), atom, any) :: Ecto.Changeset.t()
  def validate_binary_id(changeset, field, options \\ []) do
    validate_change(changeset, field, fn _, uuid ->
      case uuid(uuid) do
        {:ok, :uuid, _record_id} ->
          []

        {:error, :uuid} ->
          [
            {field,
             options[:message] ||
               Gettext.dgettext(
                 MishkaInstaller.gettext(),
                 "mishka_installer",
                 "ID should be as a UUID type."
               )}
          ]
      end
    end)
  end

  @doc """
  UUID validation.

  ## Examples

  ```elixir
  MishkaInstaller.Database.Helper.uuid(12)
  # OR
  MishkaInstaller.Database.Helper.uuid("8c512ac2-e002-4589-a93f-b479e46c249d")
  ```
  """
  @spec uuid(any) :: {:error, :uuid} | {:ok, :uuid, Ecto.UUID.t()}
  def uuid(id) do
    case Ecto.UUID.cast(id) do
      {:ok, record_id} -> {:ok, :uuid, record_id}
      _ -> {:error, :uuid}
    end
  end

  @doc """
  Helper function to keep PID alive for testing Genserver and database.

  ### Reference
  - https://elixirforum.com/t/how-to-send-sandbox-allow-for-each-dynamic-supervisor-testing/46422/4

  ## Examples

  ```elixir
  MishkaInstaller.Database.Helper.allow_if_sandbox(pid)
  ```
  """
  def allow_if_sandbox(parent_pid, orphan_msg \\ :stop) do
    if sandbox_pool?() do
      monitor_parent(parent_pid, orphan_msg)
      # this addresses #1
      Ecto.Adapters.SQL.Sandbox.allow(MishkaInstaller.repo(), parent_pid, self())
    end
  end

  defp sandbox_pool?() do
    MishkaInstaller.repo() == Ecto.Integration.TestRepo
  end

  defp monitor_parent(parent_pid, orphan_msg) do
    # this is part of addressing #2
    Process.monitor(parent_pid)

    if Process.alive?(parent_pid) do
      :ok
    else
      Logger.error("#{inspect(parent_pid)} down when booting #{inspect(self())}")
      # this addresses #3
      # the "throw" will work like an early "return"; see the GenServer docs
      throw(orphan_msg)
    end
  end

  @doc false
  def get_parent_pid(state) when is_nil(state.parent_pid), do: :ok

  def get_parent_pid(state) do
    if Mix.env() == :test do
      allow_if_sandbox(state.parent_pid)
    else
      :ok
    end
  end
end