Packages
iteraptor
1.0.2
1.15.0
1.14.0
1.13.1
1.13.0
1.12.2
1.12.1
1.12.0
1.11.0
1.10.3
1.10.2
1.10.1
1.9.0
1.8.0
1.7.3
1.7.2
1.7.1
1.7.0
1.6.1
1.6.0
1.4.1
1.4.0
1.3.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-rc1
0.7.0
0.6.2
0.6.1
0.5.2
0.5.1
0.5.0
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.4
0.2.3
0.2.2
0.2.1
0.1.1
0.1.0
This small library allows the deep iteration / mapping of Enumerables.
Current section
Files
Jump to
Current section
Files
lib/iteraptor/utils.ex
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, %{}}
iex> Iteraptor.Utils.type([foo: :bar])
{Keyword, []}
iex> Iteraptor.Utils.type([{:foo, :bar}])
{Keyword, []}
iex> Iteraptor.Utils.type(~w|foo bar|a)
{List, []}
iex> Iteraptor.Utils.type(42)
{:invalid, nil}
"""
@spec type(Map.t() | Keyword.t() | List.t() | any()) :: {atom(), %{} | [] | nil}
def type(input) do
case Enumerable.impl_for(input) do
Enumerable.List ->
{if(Keyword.keyword?(input), do: Keyword, else: List), []}
Enumerable.Map ->
{Map, %{}}
_ ->
# FIXME struct instantiation is potentially dangerous
if is_map(input),
do: {input.__struct__, struct(input.__struct__)},
else: {:invalid, nil}
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(Map.t() | Keyword.t(), List.t()) :: {:ok, {List.t(), 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!(Map.t() | Keyword.t(), List.t()) :: {List.t(), 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
@delimiter Application.get_env(:iteraptor, :delimiter, ".")
@doc false
@spec delimiter(List.t()) :: 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(binary(), Keyword.t()) :: List.t()
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(List.t(), Keyword.t()) :: binary()
def join(input, opts \\ []) when is_list(input) do
Enum.join(input, delimiter(opts))
end
@into Application.get_env(:iteraptor, :into, %{})
@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(Map.t() | Keyword.t(), {List.t(), any()}, Keyword.t()) ::
Map.t() | Keyword.t()
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(Map.t() | Keyword.t() | any()) :: true | false
def quacks_as_list(input) when is_list(input) or is_map(input) do
input
|> Enum.map(fn
{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.t() | 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(Map.t() | Keyword.t() | List.t()) :: Map.t() | Keyword.t() | List.t()
def squeeze(input) when is_map(input) or is_list(input) do
{type, into} = type(input)
{result, _} =
Enum.reduce(input, {into, 0}, fn
{k, v}, {acc, orphans} ->
{_, neu} =
case type do
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}
# FIXME opts? needed?
%{} ->
{list, list ++ Map.to_list(v)}
_ ->
{list, list ++ [v]}
end
other ->
{other, [other, v]}
end)
end
{neu, orphans}
v, {acc, orphans} ->
case type do
# FIXME raise? this cannot happen
Keyword ->
{[v | acc], orphans}
List ->
{[v | acc], orphans}
Map ->
{Map.put(acc, orphans, v), orphans + 1}
end
end)
result
|> Enum.map(fn
{k, v} when is_list(v) -> {k, v |> squeeze() |> :lists.reverse()}
{k, v} -> {k, squeeze(v)}
v -> v
end)
|> Enum.into(into)
|> try_to_list()
end
def squeeze(input), do: input
end