Packages

Allow for more complex Access pattern than is available by default in Elixir

Current section

Files

Jump to
enhanced_access lib enhanced_access.ex
Raw

lib/enhanced_access.ex

defmodule EnhancedAccess do
@moduledoc """
Documentation for `EnhancedAccess`.
"""
@doc """
Be able to access any key in a Map or a Keyword list through the Access protocol
## Examples
iex> get_in(%{a: %{b: 1}, c: %{b: 2}}, [EnhancedAccess.all_keys(), :b])
[1, 2]
iex> get_in(%{a: %{b: 1}, c: %{b: 2}}, [EnhancedAccess.all_keys()])
[%{b: 1}, %{b: 2}]
iex> get_in([a: %{b: 1}, c: %{b: 2}], [EnhancedAccess.all_keys()])
[%{b: 1}, %{b: 2}]
iex> deeper_nesting = %{a: %{b: %{c: 1}}, d: %{b: %{c: 2}}, e: %{b: 1}}
iex> get_in(deeper_nesting, [EnhancedAccess.all_keys(), :b, EnhancedAccess.optional_key(:c)])
[1, 2, nil]
iex> update_in(%{a: %{b: 1}, c: %{b: 2}}, [EnhancedAccess.all_keys(), :b], &(&1 + 1))
%{a: %{b: 2}, c: %{b: 3}}
iex> update_in([a: %{b: 1}, c: %{b: 2}], [EnhancedAccess.all_keys(), :b], &(&1 + 1))
[a: %{b: 2}, c: %{b: 3}]
iex> update_in([a: [b: 1], c: [b: 2]], [EnhancedAccess.all_keys(), :b], &(&1 + 1))
[a: [b: 2], c: [b: 3]]
iex> get_and_update_in(%{a: %{b: 1}, c: %{b: 2}}, [EnhancedAccess.all_keys(), :b], &{&1, &1 + 1})
{[1, 2], %{a: %{b: 2}, c: %{b: 3}}}
iex> pop_in(%{a: %{b: 1}, c: %{b: 2}}, [EnhancedAccess.all_keys(), :b])
{[1, 2], %{a: %{}, c: %{}}}
"""
@spec all_keys() :: Access.access_fun(data :: Access.container(), current_value :: term)
def all_keys do
&all_keys/3
end
defp all_keys(:get, data, next) when is_map(data) or is_list(data) do
Enum.map(data, fn {_, value} -> next.(value) end)
end
defp all_keys(:get_and_update, data, next) when is_map(data) do
do_all_keys(Map.to_list(data), next, _gets = [], _updates = [], %{})
end
defp all_keys(:get_and_update, data, next) when is_list(data) do
do_all_keys(data, next, _gets = [], _updates = [], [])
end
defp do_all_keys([{key, value} | rest], next, gets, updates, into) do
case next.(value) do
{get, update} -> do_all_keys(rest, next, [get | gets], [{key, update} | updates], into)
:pop -> do_all_keys(rest, next, [value | gets], updates, into)
end
end
defp do_all_keys([], _next, gets, updates, into) do
{:lists.reverse(gets), :lists.reverse(updates) |> Enum.into(into)}
end
@doc """
Be able to access any key except some key you want to skip in a Map or a Keyword list through the Access protocol
## Examples
iex> get_in(%{a: %{b: 1}, c: %{b: 2}, d: %{b: 3}}, [EnhancedAccess.skip_keys([:d]), :b])
[1, 2]
iex> get_in(%{a: %{b: 1}, c: %{b: 2}, d: %{b: 3}}, [EnhancedAccess.skip_keys([:d])])
[%{b: 1}, %{b: 2}]
iex> get_in([a: %{b: 1}, c: %{b: 2}, d: %{b: 3}], [EnhancedAccess.skip_keys([:d])])
[%{b: 1}, %{b: 2}]
iex> deeper_nesting = %{a: %{b: %{c: 1}}, d: %{b: %{c: 2}}}
iex> get_in(deeper_nesting, [EnhancedAccess.skip_keys([:d]), :b, :c])
[1]
iex> update_in(%{a: %{b: 1}, c: %{b: 2}, d: %{b: 3}}, [EnhancedAccess.skip_keys([:d]), :b], &(&1 + 1))
%{a: %{b: 2}, c: %{b: 3}, d: %{b: 3}}
iex> update_in([a: %{b: 1}, c: %{b: 2}, d: %{b: 3}], [EnhancedAccess.skip_keys([:d]), :b], &(&1 + 1))
[a: %{b: 2}, c: %{b: 3}, d: %{b: 3}]
iex> update_in([a: [b: 1], c: [b: 2], d: [b: 3]], [EnhancedAccess.skip_keys([:d]), :b], &(&1 + 1))
[a: [b: 2], c: [b: 3], d: [b: 3]]
iex> get_and_update_in(%{a: %{b: 1}, c: %{b: 2}, d: %{b: 3}}, [EnhancedAccess.skip_keys([:d]), :b], &{&1, &1 + 1})
{[1, 2], %{a: %{b: 2}, c: %{b: 3}, d: %{b: 3}}}
iex> pop_in(%{a: %{b: 1}, c: %{b: 2}, d: %{b: 3}}, [EnhancedAccess.skip_keys([:d]), :b])
{[1, 2], %{a: %{}, c: %{}, d: %{b: 3}}}
"""
@spec skip_keys(keys :: list) ::
Access.access_fun(data :: Access.container(), current_value :: term)
def skip_keys(keys) when is_list(keys) do
&skip_keys(&1, &2, &3, keys)
end
defp skip_keys(:get, data, next, keys) when is_map(data) or is_list(data) do
data
|> Enum.reject(fn {key, _} -> key in keys end)
|> Enum.map(fn {_, value} -> next.(value) end)
end
defp skip_keys(:get_and_update, data, next, keys) when is_map(data) do
do_skip_keys(Map.to_list(data), next, _gets = [], _updates = [], %{}, keys)
end
defp skip_keys(:get_and_update, data, next, keys) when is_list(data) do
do_skip_keys(data, next, _gets = [], _updates = [], [], keys)
end
defp do_skip_keys([{key, value} | rest], next, gets, updates, into, keys) do
if key in keys do
do_skip_keys(rest, next, gets, [{key, value} | updates], into, keys)
else
case next.(value) do
{get, update} ->
do_skip_keys(rest, next, [get | gets], [{key, update} | updates], into, keys)
:pop ->
do_skip_keys(rest, next, [value | gets], updates, into, keys)
end
end
end
defp do_skip_keys([], _next, gets, updates, into, _keys) do
{:lists.reverse(gets), :lists.reverse(updates) |> Enum.into(into)}
end
@doc """
Allow for optional keys in the Access protocol
## Examples
iex> get_in(%{a: %{b: 1}, c: 1}, [EnhancedAccess.all_keys(), EnhancedAccess.optional_key(:b)])
[1, nil]
iex> get_in(%{a: 1, c: %{b: 1}}, [EnhancedAccess.all_keys(), EnhancedAccess.optional_key(:b)])
[nil, 1]
iex> deeper_nesting = %{a: %{b: %{c: 1}}, d: %{b: %{c: 2}}, e: %{d: 1}}
iex> get_in(deeper_nesting, [EnhancedAccess.all_keys(), EnhancedAccess.optional_key(:b), :c])
[1, 2, nil]
iex> update_in(%{a: %{b: 1}, c: %{d: 2}}, [EnhancedAccess.all_keys(), EnhancedAccess.optional_key(:b)], &(&1 + 1))
%{a: %{b: 2}, c: %{d: 2}}
iex> update_in([a: [b: 1], c: [d: 2]], [EnhancedAccess.all_keys(), EnhancedAccess.optional_key(:b)], &(&1 + 1))
[a: [b: 2], c: [d: 2]]
iex> get_and_update_in(%{a: %{b: 1}, c: %{d: 2}}, [EnhancedAccess.all_keys(), EnhancedAccess.optional_key(:b)], &{&1, &1 + 1})
{[1, nil], %{a: %{b: 2}, c: %{d: 2}}}
iex> pop_in(%{a: %{b: 1}, c: %{d: 2}}, [EnhancedAccess.all_keys(), EnhancedAccess.optional_key(:b)])
{[1, nil], %{a: %{}, c: %{d: 2}}}
"""
@spec optional_key(term) :: Access.access_fun(data :: Access.container(), current_value :: term)
def optional_key(key) do
&optional_key(&1, &2, &3, key)
end
defp optional_key(:get, data, next, key) when is_map(data) do
value = Map.get(data, key)
if is_nil(value) do
nil
else
next.(value)
end
end
defp optional_key(:get, data, next, key) when is_list(data) do
value = Keyword.get(data, key)
if is_nil(value) do
nil
else
next.(value)
end
end
defp optional_key(:get, _data, _next, _key) do
nil
end
defp optional_key(:get_and_update, data, next, key) when is_map(data) do
value = Map.get(data, key)
if is_nil(value) do
{nil, data}
else
case next.(value) do
{get, update} -> {get, Map.put(data, key, update)}
:pop -> {value, Map.delete(data, key)}
end
end
end
defp optional_key(:get_and_update, data, next, key) when is_list(data) do
value = Keyword.get(data, key)
if is_nil(value) do
{nil, data}
else
case next.(value) do
{get, update} -> {get, Keyword.put(data, key, update)}
:pop -> {value, Keyword.delete(data, key)}
end
end
end
defp optional_key(:get_and_update, data, _next, _key) do
{nil, data}
end
end