Packages

Provides extended utility functions for working with maps, which are not covered in vanilla elixir.

Current section

Files

Jump to
maptools lib map_tools.ex
Raw

lib/map_tools.ex

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