defmodule Frenzy.Keypath do @moduledoc """ Utilities for accessing or updating values using keypaths. A keypath is a list of map keys or list indices representing the path through nested maps/lists. """ @type container() :: map() | list() @type t() :: [Map.key() | non_neg_integer()] @doc """ Gets the value in the given container at the given keypath. Raises on index out of bounds/missing map key. ## Examples iex> Frenzy.Keypath.get(%{foo: %{bar: "baz"}}, [:foo, :bar]) "baz" iex> Frenzy.Keypath.get([%{"foo" => "bar"}, %{"foo" => "baz"}], [1, "foo"]) "baz" """ @spec get(container(), t()) :: any() def get(value, []), do: value def get(map, [key | rest]) when is_map(map) do get(Map.fetch!(map, key), rest) end def get(list, [index | rest]) when is_list(list) and is_integer(index) and index >= 0 do if index >= length(list) do raise KeyError, "Index #{index} out of bounds (>= #{length(list)})" else get(Enum.at(list, index), rest) end end @doc """ Sets the vlaue in the given container at the given keypath. Raises on index out of bounds/missing map key. ## Examples iex> Frenzy.Keypath.set(%{foo: %{bar: "baz"}}, [:foo, :bar], "blah") %{foo: %{bar: "blah"}} iex> Frenzy.Keypath.set([%{"foo" => "bar"}, %{"list" => ["a", "b"]}], [1, "list", 0], "c") [%{"foo" => "bar"}, %{"list" => ["c", "b"]}] """ @spec set(container(), t(), any()) :: map() def set(map, [key], value) when is_map(map) do Map.put(map, key, value) end def set(list, [index], value) when is_list(list) and is_integer(index) and index >= 0 do if index >= length(list) do raise KeyError, "Index #{index} out of bounds (>= #{length(list)})" else List.replace_at(list, index, value) end end def set(map, [key | rest], value) when is_map(map) do Map.put(map, key, set(Map.fetch!(map, key), rest, value)) end def set(list, [index | rest], value) when is_list(list) and is_integer(index) and index >= 0 do if index >= length(list) do raise KeyError, "Index #{index} out of bounds (>= #{length(list)})" else List.replace_at(list, index, set(Enum.at(list, index), rest, value)) end end end