igniter/lib/code/map.ex
2024-06-12 19:16:03 -04:00

173 lines
4.3 KiB
Elixir

defmodule Igniter.Code.Map do
@moduledoc """
Utilities for working with maps.
"""
require Igniter.Code.Common
alias Igniter.Code.Common
alias Sourceror.Zipper
@doc "Puts a value at a path into a map, calling `updater` on the zipper at the value if the key is already present"
@spec put_in_map(
Zipper.t(),
list(term()),
term(),
(Zipper.t() -> {:ok, Zipper.t()} | :error) | nil
) ::
{:ok, Zipper.t()} | :error
def put_in_map(zipper, path, value, updater \\ nil) do
updater = updater || fn zipper -> {:ok, Zipper.replace(zipper, value)} end
do_put_in_map(zipper, path, value, updater)
end
defp do_put_in_map(zipper, [key], value, updater) do
set_map_key(zipper, key, value, updater)
end
defp do_put_in_map(zipper, [key | rest], value, updater) do
cond do
Common.node_matches_pattern?(zipper, {:%{}, _, []}) ->
{:ok,
Zipper.append_child(
zipper,
mappify([key | rest], value)
)}
Common.node_matches_pattern?(zipper, {:%{}, _, _}) ->
zipper
|> Zipper.down()
|> Igniter.Code.List.move_to_list_item(fn item ->
if Igniter.Code.Tuple.tuple?(item) do
case Igniter.Code.Tuple.tuple_elem(item, 0) do
{:ok, first_elem} ->
Common.node_matches_pattern?(first_elem, ^key)
:error ->
false
end
end
end)
|> case do
:error ->
format = map_keys_format(zipper)
value = mappify(rest, value)
{:ok,
Zipper.append_child(
zipper,
{{:__block__, [format: format], [key]}, {:__block__, [], [value]}}
)}
{:ok, zipper} ->
zipper
|> Igniter.Code.Tuple.tuple_elem(1)
|> case do
{:ok, zipper} ->
do_put_in_map(zipper, rest, value, updater)
:error ->
:error
end
end
true ->
:error
end
end
@doc "Puts a key into a map, calling `updater` on the zipper at the value if the key is already present"
@spec set_map_key(Zipper.t(), term(), term(), (Zipper.t() -> {:ok, Zipper.t()} | :error)) ::
{:ok, Zipper.t()} | :error
def set_map_key(zipper, key, value, updater) do
cond do
Common.node_matches_pattern?(zipper, {:%{}, _, []}) ->
{:ok,
Zipper.append_child(
zipper,
mappify([key], value)
)}
Common.node_matches_pattern?(zipper, {:%{}, _, _}) ->
zipper
|> Zipper.down()
|> Igniter.Code.List.move_to_list_item(fn item ->
if Igniter.Code.Tuple.tuple?(item) do
case Igniter.Code.Tuple.tuple_elem(item, 0) do
{:ok, first_elem} ->
Common.node_matches_pattern?(first_elem, ^key)
:error ->
false
end
end
end)
|> case do
:error ->
format = map_keys_format(zipper)
{:ok,
Zipper.append_child(
zipper,
{{:__block__, [format: format], [key]}, {:__block__, [], [value]}}
)}
{:ok, zipper} ->
zipper
|> Igniter.Code.Tuple.tuple_elem(1)
|> case do
{:ok, zipper} ->
updater.(zipper)
:error ->
:error
end
end
true ->
:error
end
end
defp map_keys_format(zipper) do
zipper
|> Zipper.subtree()
|> Zipper.node()
|> case do
value when is_list(value) ->
Enum.all?(value, fn
{:__block__, meta, _} ->
meta[:format] == :keyword
_ ->
false
end)
|> case do
true ->
:keyword
false ->
:map
end
_ ->
:map
end
end
@doc "Puts a value into nested maps at the given path"
def mappify([], value) do
value
end
def mappify([key | rest], value) do
format =
if is_atom(key) do
:keyword
else
:map
end
{:%{}, [], [{{:__block__, [format: format], [key]}, mappify(rest, value)}]}
end
end