lib/message_store/map_extra.ex

defmodule MessageStore.MapExtra do
  @moduledoc """
  An extra functions for Map data type.
  """

  defmodule PathError do
    defexception [:path, :term, :message]

    @impl true
    def message(%{message: nil} = exception), do: message(exception.path, exception.term)
    def message(%{message: message}), do: message

    defp message(path, term) do
      message = "path #{inspect(path)} not found"

      if term != nil do
        message <> " in: #{inspect(term)}"
      else
        message
      end
    end
  end

  @doc """
  Fetches the value for a specific `path` in the given `map`.

  If `map` contains the given `path` then its value is returned in the shape of `{:ok, value}`.
  If `map` doesn't contain `path`, :error is returned.

  ## Examples
      iex> a = %{a: 1, b: %{c: 2}}
      iex> MapExtra.fetch_in(a, [:a])
      {:ok, 1}

      iex> a = %{a: 1, b: %{c: 2}}
      iex> MapExtra.fetch_in(a, [:b, :c])
      {:ok, 2}

      iex> a = %{a: 1, b: %{c: 2}}
      iex> MapExtra.fetch_in(a, [:b, :d])
      :error
  """
  @spec fetch_in(map(), [Map.key()]) :: {:ok, Map.value()} | :error
  def fetch_in(map, path) when is_map(map) and is_list(path) do
    fetch({:ok, map}, path)
  end

  defp fetch(result, []), do: result
  defp fetch(:error, _), do: :error

  defp fetch({:ok, map}, [key | rest_path]) when is_map(map) do
    map
    |> Map.fetch(key)
    |> fetch(rest_path)
  end

  @doc """
  Fetches the value for a specific `path` in the given map, erroring out if map doesn't contain `path`.

  If map contains `path`, the corresponding value is returned.
  If map doesn't contain `path`, a `PathError` exception is raised.

  ## Examples
      iex> a = %{a: 1, b: %{c: 2}}
      iex> MapExtra.fetch_in!(a, [:a])
      1
  """
  @spec fetch_in!(map(), [Map.key()]) :: Map.value()
  def fetch_in!(map, path) do
    case fetch_in(map, path) do
      {:ok, value} -> value
      :error -> raise(PathError, path: path, term: map)
    end
  end
end