Current section

Files

Jump to
rivet_utils lib dig.ex
Raw

lib/dig.ex

defmodule Rivet.Utils.Dig do
@moduledoc """
Contributor: Brandon Gillespie
"""
import Transmogrify.As
@rx_split ~r/\.(?=[^\]]*(?:\[|$))/
@rx_index ~r/^(.*)\[{(.*[^\]]*)}\]$/
@doc """
[see also: frontend code base for mirror code in js]
keys must be a list of {direct: true|false, key: y}
where direct=true is simply obj[key], and indirect does
a search of an array for the first key/value match
NOTE: keys are always converted to atoms
iex> dig_keys("a[{b:name}].capKey.d")
[
%{ direct: true, key: :a },
%{ direct: false, key: [:b, "name"] },
%{ direct: true, key: :capKey },
%{ direct: true, key: :d }
]
iex> dig_keys("a[{b:name}].capKey.d", snake_case: true)
[
%{ direct: true, key: :a },
%{ direct: false, key: [:b, "name"] },
%{ direct: true, key: :cap_key },
%{ direct: true, key: :d }
]
"""
def dig_keys(key, opts \\ [])
def dig_keys(key, opts) when is_binary(key) do
key_to_atom =
if Keyword.get(opts, :snake_case, false),
do: &as_key!/1,
else: &as_atom!/1
Regex.split(@rx_split, key)
|> Enum.reduce([], fn e, acc ->
case Regex.run(@rx_index, e) do
[_, key, value] ->
[subkey, subvalue] = Regex.split(~r/\s*:\s*/, value)
acc ++
[
%{direct: true, key: key_to_atom.(key)},
%{direct: false, key: [key_to_atom.(subkey), subvalue]}
]
_ ->
acc ++ [%{direct: true, key: key_to_atom.(e)}]
end
end)
end
def dig_keys(key, _opts) when is_list(key) do
Enum.map(key, &%{direct: true, key: &1})
end
@doc """
dig a value using pre-parsed digKeys
"""
def digx(dict, key, default \\ nil)
def digx(dict, [key], default) do
case digx_getvalue(dict, key) do
nil ->
default
result ->
result
end
end
def digx(dict, [key | keySet], default) do
# need better way to pipeline this
case digx_getvalue(dict, key) do
value when is_map(value) or is_list(value) ->
digx(value, keySet, default)
_ ->
default
end
end
defp digx_getvalue(dict, %{direct: true, key: key}),
do: Map.get(dict, key)
defp digx_getvalue(list, %{direct: false, key: [key, value]}),
do: Enum.find(list, fn v -> Map.get(v, key) == value end)
@doc """
dig a value using a string index digKeys notation
This takes both a.b.c and a[{index}].b.c, where index
in the second case is an indirect key/value lookup
iex> data = %{a: [ %{c: 1}, %{d: 2}, %{b: "name", c: %{d: "meep"}}]}
iex> dig(data, "a[{b:name}].c.d")
"meep"
iex> dig(data, "a[{b:name}].d.d")
nil
iex> dig(data, "a[{b:bigglesworth}].c.d")
nil
"""
def dig(dict, key, default \\ nil), do: digx(dict, dig_keys(key), default)
################################################################################
## top level: just put in the value
def dugx(dict, [%{direct: true, key: key}], value),
do: {:ok, Map.put(dict, key, value)}
## # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
## mid-level direct key
def dugx(dict, [%{direct: true, key: key} | rest], value) do
with {:ok, child} <- dugx(Map.get(dict, key, %{}), rest, value) do
{:ok, Map.put(dict, key, child)}
end
end
## # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
## mid-level indirect array match key
def dugx(list, [%{direct: false, key: [key, match]} | rest], value) when is_list(list) do
case Enum.find_index(list, fn e -> e[key] == match end) do
nil ->
# append
with {:ok, child} <- dugx(Map.new([{key, match}]), rest, value) do
{:ok, list ++ [child]}
end
x ->
with {:ok, child} <- dugx(Enum.at(list, x), rest, value) do
{:ok, List.replace_at(list, x, child)}
end
end
end
def dugx(_, _, _), do: {:error, "Unsupported key type"}
################################################################################
@doc """
This is the inverse of dig -- put a value in place
iex> data = %{a: [ %{c: 1}, %{d: 2}, %{b: "road", c: %{d: "meep"}}]}
iex> dug(data, "a[{b:road}].c.d", "meep meep")
{:ok, %{a: [ %{c: 1}, %{d: 2}, %{b: "road", c: %{d: "meep meep"}}]}}
iex> dug(data, "a[{b:runner}].c.d", "meep meep")
{:ok, %{a: [%{c: 1}, %{d: 2}, %{b: "road", c: %{d: "meep"}}, %{b: "runner", c: %{d: "meep meep"}}]}}
iex> dug(%{i: 10}, "i", 5)
{:ok, %{i: 5}}
iex> dug(%{a: [%{b: "z"}, %{b: "z"}]}, "a[{b:z}].c", 5)
{:ok, %{a: [%{b: "z", c: 5}, %{b: "z"}]}}
"""
def dug(dict, key, default \\ nil), do: dugx(dict, dig_keys(key), default)
def dug!(dict, key, default \\ nil) do
with {:ok, result} <- dugx(dict, dig_keys(key), default) do
result
end
end
# simplified, less functional, more efficient/focused - gives a default value which get_in doesn't
def dig_in(dict, keys, def_val \\ nil) do
case get_in(dict, keys) do
nil -> def_val
pass -> pass
end
end
end