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