defmodule Iteraptor.Utils do
@moduledoc "Helper functions to update nested terms"
defmodule Unsupported do
@moduledoc """
An exception to be thrown from banged methods of `Iteraptor`.
Sooner or later we’ll support everything, that’s why meanwhile
we raise `Unsupported` if something goes wrong.
"""
defexception [:term, :function, :message]
def exception(term: term, function: function) do
message = "Unsupported term #{inspect(term)} in call to #{function}."
%Iteraptor.Utils.Unsupported{term: term, function: function, message: message}
end
end
@doc """
Determines the type of the given term.
## Examples:
iex> Iteraptor.Utils.type(%{foo: :bar})
{Map, %{foo: :bar}, %{}}
iex> Iteraptor.Utils.type([foo: :bar])
{Keyword, [foo: :bar], []}
iex> Iteraptor.Utils.type([{:foo, :bar}])
{Keyword, [{:foo, :bar}], []}
iex> Iteraptor.Utils.type(~w|foo bar|a)
{List, [:foo, :bar], []}
iex> Iteraptor.Utils.type(42)
:error
"""
@spec type(%{} | keyword() | list() | any()) :: {atom(), any(), any()} | :error
def type(input) do
case {input, Enumerable.impl_for(input), Iteraptable.impl_for(input)} do
{%MapSet{}, _, _} ->
{MapSet, input, MapSet.new()}
{%Iteraptor.Array{}, _, _} ->
{Iteraptor.Array, input, Iteraptor.Array.new()}
{_, Enumerable.List, _} ->
{if(Keyword.keyword?(input), do: Keyword, else: List), input, []}
{_, Enumerable.Map, _} ->
{Map, input, %{}}
{_, _, i} when not is_nil(i) ->
{i.type(input), i.to_enumerable(input), i.to_collectable(input)}
{_, _, _} ->
if is_map(input),
do: {input.__struct__, Map.from_struct(input), %{}},
else: :error
end
end
@doc """
Digs the leaf value in the nested keyword / map.
## Examples:
iex> Iteraptor.Utils.dig(%{k1: %{k2: %{k3: :value}}})
{:ok, {[:k1, :k2, :k3], :value}}
iex> Iteraptor.Utils.dig([k1: [k2: [k3: :value]]])
{:ok, {[:k1, :k2, :k3], :value}}
iex> Iteraptor.Utils.dig([k1: :value, k2: :value])
{:error, [k1: :value, k2: :value]}
iex> Iteraptor.Utils.dig([k1: %{k2: [k3: :value]}])
{:ok, {[:k1, :k2, :k3], :value}}
"""
@spec dig(%{} | keyword(), keyword()) :: {:ok, {list(), any()}} | {:error, any()}
def dig(input, acc \\ [])
def dig(_, {:error, _} = error), do: error
def dig(input, acc) when is_map(input) do
case Map.keys(input) do
[k] -> dig(input[k], [k | acc])
_ -> {:error, input}
end
end
def dig([{k, v}], acc), do: dig(v, [k | acc])
def dig(input, _) when is_list(input), do: {:error, input}
def dig(input, acc), do: {:ok, {:lists.reverse(acc), input}}
@spec dig!(%{} | keyword(), keyword()) :: {list(), any()} | no_return()
def dig!(input, acc \\ []) do
case dig(input, acc) do
{:ok, result} -> result
{:error, term} -> raise Unsupported, term: term, function: "Iteraptor.Utils.dig/2"
end
end
if Version.compare(System.version(), "1.10.0") == :lt do
@delimiter apply(Application, :get_env, [:iteraptor, :delimiter, "."])
else
@delimiter Application.compile_env(:iteraptor, :delimiter, ".")
end
@doc false
@spec delimiter(list()) :: binary()
def delimiter(opts) when is_list(opts), do: opts[:delimiter] || @delimiter
@doc false
@spec smart_convert(any()) :: integer() | binary() | atom()
def smart_convert(value) do
case value |> to_string() |> Integer.parse() do
{value, ""} -> value
_ -> String.to_existing_atom(value)
end
end
@doc """
Splits the string by delimiter, possibly converting the keys to symbols.
## Examples:
iex> Iteraptor.Utils.split("a.b.c.d", transform: :none)
["a", "b", "c", "d"]
iex> Iteraptor.Utils.split("a_b_c_d", delimiter: "_")
["a", "b", "c", "d"]
iex> Iteraptor.Utils.split("a.b.c.d", transform: :unsafe)
[:a, :b, :c, :d]
iex> Iteraptor.Utils.split("a.b.c.d", transform: :safe)
[:a, :b, :c, :d]
"""
@spec split(input :: binary(), opts :: keyword()) :: [binary() | atom()]
def split(input, opts \\ []) when is_binary(input) do
result = String.split(input, delimiter(opts))
case opts[:transform] do
:safe -> Enum.map(result, &String.to_existing_atom/1)
:unsafe -> Enum.map(result, &String.to_atom/1)
_ -> result
end
end
@doc """
Joins the array of keys into the string using delimiter.
## Examples:
iex> Iteraptor.Utils.join(~w|a b c d|)
"a.b.c.d"
iex> Iteraptor.Utils.join(~w|a b c d|, delimiter: "_")
"a_b_c_d"
"""
@spec join(Enum.t(), keyword()) :: binary()
def join(input, opts \\ []) when is_list(input),
do: Enum.join(input, delimiter(opts))
if Version.compare(System.version(), "1.10.0") == :lt do
@into apply(Application, :get_env, [:iteraptor, :into, %{}])
else
@into Application.compile_env(:iteraptor, :into, %{})
end
@doc """
Safe put the value deeply into the term nesting structure. Creates
all the intermediate keys if needed.
## Examples:
iex> Iteraptor.Utils.deep_put_in(%{}, {~w|a b c|a, 42})
%{a: %{b: %{c: 42}}}
iex> Iteraptor.Utils.deep_put_in(%{a: %{b: %{c: 42}}}, {~w|a b d|a, :foo})
%{a: %{b: %{c: 42, d: :foo}}}
iex> Iteraptor.Utils.deep_put_in(%{a: %{b: [c: 42]}}, {~w|a b d|a, :foo})
%{a: %{b: [c: 42, d: :foo]}}
iex> Iteraptor.Utils.deep_put_in(%{a: %{b: [42]}}, {~w|a b|a, :foo})
%{a: %{b: [42, :foo]}}
iex> Iteraptor.Utils.deep_put_in(%{a: [:foo, %{b: 42}]}, {~w|a b|a, :foo})
%{a: [:foo, %{b: 42}, {:b, :foo}]}
"""
@spec deep_put_in(%{} | keyword(), {list(), any()}, keyword()) :: %{} | keyword()
def deep_put_in(target, key_value, opts \\ [])
def deep_put_in(target, {[key], value}, _opts) do
put_in(target, [key], value)
end
def deep_put_in(target, {key, value}, opts) when is_list(key) do
into = opts[:into] || @into
[tail | head] = :lists.reverse(key)
head = :lists.reverse(head)
{_, target} =
Enum.reduce(head, {[], target}, fn k, {keys, acc} ->
keys = keys ++ [k]
{_, value} = get_and_update_in(acc, keys, &{&1, if(is_nil(&1), do: into, else: &1)})
{keys, value}
end)
case get_in(target, key) do
nil ->
{_, result} =
get_and_update_in(target, head, fn
nil -> {nil, Enum.into([{tail, value}], into)}
curr when is_map(curr) -> {curr, Map.put(curr, tail, value)}
curr when is_list(curr) -> {curr, curr ++ [{tail, value}]}
curr -> {curr, [curr, {tail, value}]}
end)
result
curr when is_list(curr) ->
put_in(target, key, curr ++ [value])
curr when is_map(curr) ->
put_in(target, key, Map.to_list(curr) ++ [value])
curr ->
put_in(target, key, [curr, value])
end
end
@doc """
Checks if the map/keyword looks like a normal list.
## Examples:
iex> Iteraptor.Utils.quacks_as_list(%{"0" => :foo, 1 => :bar})
true
iex> Iteraptor.Utils.quacks_as_list([{:"1", :bar}, {:"0", :foo}])
true
iex> Iteraptor.Utils.quacks_as_list(%{foo: :bar})
false
iex> Iteraptor.Utils.quacks_as_list(%{"5" => :foo, "1" => :bar})
false
iex> Iteraptor.Utils.quacks_as_list(42)
false
"""
@spec quacks_as_list(%{} | keyword() | any()) :: true | false
def quacks_as_list(input) when is_list(input) or is_map(input) do
input
|> Enum.map(fn
{k, _} when is_atom(k) or is_binary(k) or is_number(k) ->
case k |> to_string() |> Integer.parse() do
{value, ""} -> value
_ -> nil
end
_ ->
nil
end)
|> Enum.sort() == 0..(Enum.count(input) - 1) |> Enum.to_list()
end
def quacks_as_list(_), do: false
@doc """
Gently tries to create a linked list out of input, returns input if it
cannot be safely converted to the list.
## Examples:
iex> Iteraptor.Utils.try_to_list(%{"0" => :foo, 1 => :bar})
[:foo, :bar]
iex> Iteraptor.Utils.try_to_list([{:"1", :bar}, {:"0", :foo}])
[:foo, :bar]
iex> Iteraptor.Utils.try_to_list(%{foo: :bar})
%{foo: :bar}
iex> Iteraptor.Utils.try_to_list(%{"5" => :foo, "1" => :bar})
%{"5" => :foo, "1" => :bar}
"""
@spec try_to_list(any()) :: list() | any()
def try_to_list(input) do
if quacks_as_list(input) do
input
|> Enum.sort(fn {k1, _}, {k2, _} ->
String.to_integer(to_string(k1)) < String.to_integer(to_string(k2))
end)
|> Enum.map(fn {_, v} -> v end)
else
input
end
end
@doc """
Squeezes the nested structure merging same keys.
## Examples:
#iex> Iteraptor.Utils.squeeze([foo: [bar: 42], foo: [baz: 3.14]])
#[foo: [bar: 42, baz: 3.14]]
iex> Iteraptor.Utils.squeeze([foo: %{bar: 42}, foo: %{baz: 3.14}])
[foo: %{bar: 42, baz: 3.14}]
iex> Iteraptor.Utils.squeeze([foo: %{bar: 42}, foo: :baz])
[foo: [%{bar: 42}, :baz]]
iex> Iteraptor.Utils.squeeze([a: [b: [c: 42]], a: [b: [d: 3.14]]])
[a: [b: [c: 42, d: 3.14]]]
iex> Iteraptor.Utils.squeeze([a: [b: [c: 42]], a: [b: %{d: 3.14}]])
[a: [b: [c: 42, d: 3.14]]]
iex> Iteraptor.Utils.squeeze([a: [b: [c: :foo]], a: [b: [c: 3.14]]])
[a: [b: [c: [:foo, 3.14]]]]
iex> Iteraptor.Utils.squeeze([a: [b: [:foo, :bar]], a: [b: [c: 3.14]]])
[a: [b: [:foo, :bar, {:c, 3.14}]]]
iex> Iteraptor.Utils.squeeze([a: [:foo, :bar], a: [b: [c: 3.14]]])
[a: [:foo, :bar, {:b, [c: 3.14]}]]
"""
@spec squeeze(%{} | keyword() | list() | Access.t(), keyword()) :: %{} | keyword() | list()
# credo:disable-for-lines:59
def squeeze(input, opts \\ [])
def squeeze(input, opts) when is_map(input) or is_list(input) do
{type, input, into} = type(input)
{result, _} =
Enum.reduce(input, {into, 0}, fn
{k, v}, {acc, orphans} ->
{_, neu} =
case type do
MapSet ->
{nil, MapSet.put(acc, {k, v})}
Iteraptor.Array ->
{nil, Iteraptor.Array.append(acc, {k, v})}
List ->
{nil, [{k, v} | acc]}
_ ->
get_and_update_in(acc, [k], fn
nil ->
{nil, v}
map when is_map(map) ->
case v do
%{} -> {map, Map.merge(map, v)}
_ -> {map, [map, v]}
end
list when is_list(list) ->
case v do
[] -> {list, list}
[_ | _] -> {list, list ++ v}
%{} -> {list, list ++ Map.to_list(v)}
_ -> {list, list ++ [v]}
end
other ->
{other, [other, v]}
end)
end
{neu, orphans}
v, {acc, orphans} ->
case type do
Keyword -> {[v | acc], orphans}
List -> {[v | acc], orphans}
Map -> {Map.put(acc, orphans, v), orphans + 1}
end
end)
result =
result
|> Enum.into(into, fn
{k, v} when is_list(v) -> {k, v |> squeeze(opts) |> :lists.reverse()}
{k, v} -> {k, squeeze(v, opts)}
v -> v
end)
|> try_to_list()
if opts[:structs] == :keep && is_map(result) and type != Map,
do: struct(type, result),
else: result
end
def squeeze(input, _opts), do: input
@doc false
def struct_checker(env, _bytecode), do: env.module.__struct__
end