defmodule MapTools do @moduledoc """ Provides extended utility functions for working with maps, which are not covered in vanilla elixir. Functionality preview: - remove all `nil` value entries - replace a map's key with another key - merge maps togehter recursively - overwriting existing values - accumulating values - getting a value by a path ## Examples ``` iex> MapTools.key_replace(%{a: 1, b: 2, c: 3}, %{a: :alpha, b: :beta}) %{alpha: 1, beta: 2, c: 3} ``` ``` iex> MapTools.merge_recursive(%{a: 1, b: %{c: 2}}, %{a: 3, b: %{d: 4}}) %{a: 3, b: %{c: 2, d: 4}} ``` ``` iex> MapTools.remove_nil(%{a: 1, b: nil, c: 3}) %{a: 1, c: 3} ``` """ @doc """ Replaces keys in a map according to a mapping of old keys to new keys. ## Examples ``` iex> MapTools.key_replace(%{a: 1, b: 2}, %{a: :alpha, b: :beta}) %{alpha: 1, beta: 2} ``` ``` iex> MapTools.key_replace(%{a: 1, b: 2, c: 3}, %{a: :alpha}) %{alpha: 1, b: 2, c: 3} ``` """ @spec key_replace(map(), %{atom => atom}) :: map() def key_replace(%{} = map, %{} = mapping_keys) do map |> Enum.map(fn {k, v} -> {mapping_keys[k] || k, v} end) |> Enum.into(%{}) end @doc """ Deeply merges two maps. When a key exists in both maps and both values are maps, the values are recursively merged. Otherwise, the value from the second map is used. ## Examples ``` iex> MapTools.merge_recursive(%{a: 1, b: %{c: 2}}, %{a: 3, b: %{d: 4}}) %{a: 3, b: %{c: 2, d: 4}} ``` ``` iex> MapTools.merge_recursive(%{a: 1}, %{b: 2}) %{a: 1, b: 2} ``` """ @spec merge_recursive(map(), map()) :: map() def merge_recursive(%{} = map1, %{} = map2) do Map.merge(map1, map2, fn _, m1, m2 -> merge_recursive_callback(m1, m2) end) end @doc """ Deeply merges two maps and creates a list of values when a key exists in both maps. ## Examples ``` iex> MapTools.merge_recursive(%{a: 1, b: 2}, %{a: 3, c: 4}, :recursive_accumulate) %{a: [1, 3], b: 2, c: 4} ``` ``` iex> MapTools.merge_recursive(%{a: 1}, %{b: 2}, :recursive_accumulate) %{a: 1, b: 2} ``` """ @spec merge_recursive(map(), map(), :recursive_accumulate) :: map() def merge_recursive(%{} = map1, %{} = map2, :recursive_accumulate) do Map.merge(map1, map2, fn _key, value1, value2 -> [value1, value2] end) end defp merge_recursive_callback(%{} = value1, %{} = value2) do merge_recursive(value1, value2) end defp merge_recursive_callback(_value1, value2) do value2 end @doc """ Removes all nil values from the map. ## Examples ``` iex> MapTools.remove_nil(%{a: 1, b: nil, c: 3}) %{a: 1, c: 3} ``` ``` iex> MapTools.remove_nil(%{a: nil, b: nil}) %{} ``` """ @spec remove_nil(map()) :: map() def remove_nil(%{} = map) do map |> Map.reject(fn {_, value} -> is_nil(value) end) end @doc """ Retrieves a value from a nested map using a list of keys (path). Returns :not_found if the path does not exist or if any intermediate key is missing. Returns nil if the value at the path is nil. ## Examples ``` iex> MapTools.get_by_path(%{a: %{b: %{c: 123}, d: 234}, e: 345}, [:a, :b, :c]) 123 ``` ``` iex> MapTools.get_by_path(%{a: %{b: %{c: 123}}}, [:a, :c]) :not_found ``` ``` iex> MapTools.get_by_path(%{a: %{b: nil}}, [:a, :b]) nil ``` """ @spec get_by_path(map(), [atom()]) :: any() | :not_found def get_by_path(map, []), do: map def get_by_path(map, [key | rest]) do case Map.fetch(map, key) do {:ok, value} -> if rest == [] do value else get_by_path(value, rest) end :error -> :not_found end end end