defmodule XmlBuilder.Access do
@moduledoc """
Provides a function-based `Access` implementation. This allows to get an access
to deeply nested elemetns via `Kernel.get_in/2`, `Kernel.pop_in/2`, `Kernel.put_in/3`,
`Kernel.update_in/3`, and `Kernel.get_and_update_in/3`.
**Example:**
iex> get_in({:person, %{id: 1}, [{:data, %{}, [{:name, %{}, "John"}]}]},
...> [XmlBuilder.Access.key(:data), XmlBuilder.Access.key(:name), XmlBuilder.Access.key()])
"John"
iex> update_in({:persons, %{}, [{:name, %{}, "John"}, {:name, %{}, "Jane"}]},
...> [XmlBuilder.Access.key({:name, -1}), XmlBuilder.Access.key()], fn _ -> "Mary" end)
{:persons, %{}, [{:name, %{}, "John"}, {:name, %{}, "Mary"}]}
Negative indices are supported, `-1` for the last element, `-2` for next to the last etc.
"""
@typedoc """
Nested elements of any node are in general accessible by `{name, index}` tuple.
When a single name atom passed as an argument, the implementation assumes
index zero.
"""
@type maybe_ordered_key ::
nil | atom() | {atom(), integer()}
@doc """
Default `Access` function implementation accepting default values.
**Examples:**
iex> # simple value
iex> get_in({:person, %{id: 1}, 42}, [XmlBuilder.Access.key()])
42
iex> put_in({:person, %{id: 1}, nil}, [XmlBuilder.Access.key()], 42)
{:person, %{id: 1}, 42}
iex> update_in({:person, %{id: 1}, nil},
...> [XmlBuilder.Access.key()], fn _ -> 42 end)
{:person, %{id: 1}, 42}
iex> get_and_update_in({:person, %{id: 1}, nil},
...> [XmlBuilder.Access.key()], fn old -> {old, 42} end)
{nil, {:person, %{id: 1}, 42}}
iex> # nested element
iex> get_in({:person, %{id: 1}, [{:name, %{}, "John"}]},
...> [XmlBuilder.Access.key(:name)])
{:name, %{}, "John"}
iex> put_in({:person, %{id: 1}, [{:name, %{}, "John"}]},
...> [XmlBuilder.Access.key(:name)], "Mary")
{:person, %{id: 1}, [{:name, %{}, "Mary"}]}
iex> update_in({:person, %{id: 1}, [{:name, %{}, "John"}]},
...> [XmlBuilder.Access.key(:name)], fn _ -> "Mary" end)
{:person, %{id: 1}, [{:name, %{}, "Mary"}]}
iex> get_and_update_in({:person, %{id: 1}, [{:name, %{}, "John"}]},
...> [XmlBuilder.Access.key(:name)], fn {_, _, old} -> {old, "Mary"} end)
{"John", {:person, %{id: 1}, [{:name, %{}, "Mary"}]}}
"""
@spec key(key :: maybe_ordered_key()) ::
Access.access_fun(data :: {atom(), map(), list() | any()}, get_value :: term)
def key(key \\ nil)
def key(nil) do
fn
:get, {_name, _attrs, value}, fun ->
fun.(value)
:get_and_update, {name, attrs, value}, fun ->
case fun.(value) do
:pop ->
{value, {name, attrs, nil}}
{old, updated} ->
{old, {name, attrs, updated}}
end
end
end
def key(key) when is_atom(key), do: key({key, 0})
def key({key, index}) when is_atom(key) and is_integer(index) do
fn
:get, {_name, _attrs, value}, fun when is_list(value) ->
{value, idx} = if index < 0, do: {Enum.reverse(value), -index - 1}, else: {value, index}
value
|> Enum.reduce_while({0, nil}, fn
{^key, _, _} = e, {^idx, nil} -> {:halt, {idx, e}}
{^key, _, _}, {idx, nil} -> {:cont, {idx + 1, nil}}
_, acc -> {:cont, acc}
end)
|> elem(1)
|> case do
{^key, attrs, value} ->
value = if index < 0 and is_list(value), do: Enum.reverse(value), else: value
fun.({key, attrs, value})
nil ->
fun.(nil)
end
:get_and_update, {name, attrs, value}, fun when is_list(value) ->
{value, idx} = if index < 0, do: {Enum.reverse(value), -index - 1}, else: {value, index}
{updated_at, {old, updated}} =
Enum.reduce(value, {0, {nil, []}}, fn
{^key, attrs, value}, {^idx, {nil, acc}} ->
updated =
case fun.({key, attrs, value}) do
:pop -> {{key, attrs, value}, acc}
{old, {key, attrs, updated}} -> {old, [{key, attrs, updated} | acc]}
{old, updated} -> {old, [{key, attrs, updated} | acc]}
end
{idx + 1, updated}
{^key, _, _} = matched, {idx, {value, acc}} ->
{idx + 1, {value, [matched | acc]}}
any, {idx, {value, acc}} ->
{idx, {value, [any | acc]}}
end)
updated = if index >= 0, do: Enum.reverse(updated), else: updated
if updated_at > idx do
{old, {name, attrs, updated}}
else
{old_value, updated_value} =
case fun.({key, attrs, nil}) do
:pop -> {{key, attrs, nil}, []}
{old, {key, attrs, updated}} -> {old, [{key, attrs, updated}]}
{old, updated} -> {old, [{key, attrs, updated}]}
end
{old_value, {name, attrs, updated ++ updated_value}}
end
end
end
end