Packages
iteraptor
0.5.0
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.ex
defmodule Iteraptor do
@moduledoc """
`Iteraptor` makes complicated nested structures (currently `map`s, `list`s
and somehow `Keyword`s) iteration easier.
## Usage
### `to_flatmap`:
iex> %{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}} |> Iteraptor.to_flatmap
%{"a.b.c" => 42, "a.b.d.0" => nil, "a.b.d.1" => 42, "a.e.0" => :f, "a.e.1" => 42}
### `from_flatmap`:
iex> %{"a.b.c" => 42, "a.b.d.0" => nil, "a.b.d.1" => 42, "a.e.0" => :f, "a.e.1" => 42}
...> |> Iteraptor.from_flatmap
%{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}}
### `each`:
%{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}}
|> Iteraptor.each(fn({k, v}) ->
Logger.debug(k <> " ⇒ " <> inspect(v))
end)
Returning:
a.b.c ⇒ 42
a.b.d.0 ⇒ nil
a.b.d.1 ⇒ 42
a.e.0 ⇒ :f
a.e.1 ⇒ 42
"""
@joiner "."
@struct_joiner "%"
@doc """
Build a flatmap out of nested structure, concatenating the names of keys.
%{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}} |> Iteraptor.to_flatmap
%{"a.b.c": 42, "a.b.d.0": nil, "a.b.d.1": 42, "a.e.0": :f, "a.e.1": 42}
Lists are handled gracefully, index is used as a key in resulting map.
## Parameters
- `input`: nested map/list/keyword/struct to be flattened.
- `joiner`: the character to be used to join keys while flattening,
_optional_, default value is `"."`;
e.g. `%{a: {b: 42}}` will be flattened to `%{"a.b" => 42}`.
## Examples
iex> [:a, 42] |> Iteraptor.to_flatmap
%{"0": :a, "1": 42}
iex> %{a: 42} |> Iteraptor.to_flatmap
%{a: 42}
iex> %{a: 42, b: 42} |> Iteraptor.to_flatmap
%{a: 42, b: 42}
iex> %{a: %{b: 42}, d: 42} |> Iteraptor.to_flatmap
%{"a.b" => 42, d: 42}
iex> %{a: [:b, 42], d: 42} |> Iteraptor.to_flatmap
%{"a.0" => :b, "a.1" => 42, d: 42}
iex> %{a: %{b: [:c, 42]}, d: 42} |> Iteraptor.to_flatmap
%{"a.b.0" => :c, "a.b.1" => 42, d: 42}
iex> %{a: %{b: 42}} |> Iteraptor.to_flatmap
%{"a.b" => 42}
iex> %{a: %{b: %{c: 42}}} |> Iteraptor.to_flatmap
%{"a.b.c" => 42}
iex> %{a: %{b: %{c: 42}}, d: 42} |> Iteraptor.to_flatmap
%{"a.b.c" => 42, d: 42}
iex> [a: [b: [c: 42]], d: 42] |> Iteraptor.to_flatmap
%{"a.b.c" => 42, d: 42}
iex> [a: [[:b], 42], d: 42] |> Iteraptor.to_flatmap
%{"a.0.0" => :b, "a.1" => 42, d: 42}
iex> %{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}} |> Iteraptor.to_flatmap
%{"a.b.c" => 42, "a.b.d.0" => nil, "a.b.d.1" => 42, "a.e.0" => :f, "a.e.1" => 42}
iex> %Struct1{field1: %Struct2{field2: [%{a: 42}, :b]}} |> Iteraptor.to_flatmap
%{"Struct1%field1.Struct2%field2.0.a" => 42, "Struct1%field1.Struct2%field2.1" => :b}
"""
def to_flatmap(input, joiner \\ @joiner) when is_map(input) or is_list(input) do
process(input, joiner)
end
@doc """
Build a nested structure out of a flatmap given, decomposing the names of keys
and handling lists carefully.
%{"a.b.c": 42, "a.b.d.0": nil, "a.b.d.1": 42, "a.e.0": :f, "a.e.1": 42} |> Iteraptor.from_flatmap
%{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}}
## Parameters
- `input`: flat map to be “expanded” to nested maps/lists.
- `joiner`: the character to be used to “un-join” keys while flattening,
_optional_, default value is `"."`;
e.g. `%{"a.b" => 42}` will be unveiled to `%{a: {b: 42}}`.
## Examples
iex> %{"a.b.c": 42} |> Iteraptor.from_flatmap
%{a: %{b: %{c: 42}}}
iex> %{"a.b.c": 42, "a.b.d": 42} |> Iteraptor.from_flatmap
%{a: %{b: %{c: 42, d: 42}}}
iex> %{"a.b.c": 42, "a.b.d": 42, "a.e": 42} |> Iteraptor.from_flatmap
%{a: %{b: %{c: 42, d: 42}, e: 42}}
iex> %{"0": 42, "1": 42} |> Iteraptor.from_flatmap
[42, 42]
iex> %{"1": :a1, "0": :a0, "2": :a2, "3": :a3, "4": :a4, "5": :a5,
...> "6": :a6, "7": :a7, "8": :a8, "9": :a9, "10": :a10, "11": :a11}
...> |> Iteraptor.from_flatmap
[:a0, :a1, :a2, :a3, :a4, :a5, :a6, :a7, :a8, :a9, :a10, :a11]
iex> %{"0.a": 42, "0.b": 42} |> Iteraptor.from_flatmap
[%{a: 42, b: 42}]
iex> %{"a.0.0" => :b, "a.1" => 42, d: 42} |> Iteraptor.from_flatmap
%{a: [[:b], 42], d: 42}
iex> %{"a.b.c": 42, "a.b.d.0": nil, "a.b.d.1": 42, "a.e.0": :f, "a.e.1": 42}
...> |> Iteraptor.from_flatmap
%{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}}
iex> %{"Struct1%field1.Struct2%field2.0.a" => 42, "Struct1%field1.Struct2%field2.1" => :b}
...> |> Iteraptor.from_flatmap
%Struct1{field1: %Struct2{field2: [%{a: 42}, :b]}}
"""
def from_flatmap(input, joiner \\ @joiner) when is_map(input) do
unprocess(input, joiner)
end
@doc """
Iterates the given nested structure, calling the callback provided on each
value. The key returned is a concatenated names of all the parent keys
(and/or indices in a case of an array.)
The return value is the result of call to `to_flatmap`.
## Parameters
- `input`: nested map/list/keyword to be walked through.
- `joiner`: the character to be used to join keys while flattening,
is returned to the callback as iterated key name;
_optional_, default value is `"."`;
- `fun`: callback to be called on each _value_;
e.g. on `%{a: {b: 42}}` will be called once, with tuple `{"a.b", 42}`.
## Examples
iex> %{a: %{b: %{c: 42}}} |> Iteraptor.each(fn {k, v} -> Logger.debug(inspect({k, v})) end)
%{"a.b.c" => 42}
"""
def each(input, joiner \\ @joiner, fun) do
unless is_function(fun, 1), do: raise "Function or arity fun/1 is required"
process(input, joiner, nil, "", %{}, fun)
end
##############################################################################
@lint [{Credo.Check.Refactor.FunctionArity, false}]
defp process(input, joiner, type \\ nil, prefix \\ "", acc \\ %{}, fun \\ nil)
### -----------------------------------------------------------------------###
@lint [{Credo.Check.Refactor.FunctionArity, false}]
defp process(input, joiner, nil, prefix, acc, fun) do
type = case Enumerable.impl_for(input) do
Enumerable.List -> if Keyword.keyword?(input), do: :keyword, else: :list
Enumerable.Map -> :map
_ -> if is_map(input), do: :struct, else: :unknown
end
process(input, joiner, type, prefix, acc, fun)
end
### -----------------------------------------------------------------------###
@lint [{Credo.Check.Refactor.FunctionArity, false}]
defp process(_, _, :unknown, prefix, _, _) do
raise ArgumentError, message: "Unsupported data type found at prefix: #{prefix}"
end
@lint [{Credo.Check.Refactor.FunctionArity, false}]
defp process(input, joiner, :map, prefix, acc, fun) do
input |> Enum.reduce(acc, fn({k, v}, memo) ->
prefix = join(prefix, k, joiner)
if is_map(v) or is_list(v) do
process(v, joiner, nil, prefix, memo, fun)
else
unless is_nil(fun), do: fun.({prefix, v})
Map.put memo, prefix, v
end
end)
end
@lint [{Credo.Check.Refactor.FunctionArity, false}]
defp process(input, joiner, :keyword, prefix, acc, fun) do
input |> Enum.into(%{}) |> process(joiner, nil, prefix, acc, fun)
end
@lint [{Credo.Check.Refactor.FunctionArity, false}]
defp process(input, joiner, :list, prefix, acc, fun) do
input
|> Enum.with_index
|> Enum.map(fn({k, v}) -> {v, k} end)
|> Enum.into(%{})
|> process(joiner, nil, prefix, acc, fun)
end
@lint [{Credo.Check.Refactor.FunctionArity, false}]
defp process(input, joiner, :struct, prefix, acc, fun) do
struct_name = input.__struct__ |> inspect |> String.replace(".", @struct_joiner)
input
|> Map.keys
|> Enum.filter(fn e -> e != :__struct__ end)
|> Enum.map(fn e ->
{"#{struct_name}%#{e}", get_in(input, [Access.key!(e)])}
end)
|> Enum.into(%{})
|> process(joiner, nil, prefix, acc, fun)
end
##############################################################################
defp unprocess(input, joiner, fun \\ nil)
### -----------------------------------------------------------------------###
defp unprocess(input, joiner, fun) when is_map(input) do
input
|> Enum.reduce(%{}, fn({key, value}, acc) ->
put_or_update(acc, joiner, key, value, fun)
end)
|> imply_lists(joiner)
end
##############################################################################
defp join(l, r \\ "", joiner \\ @joiner)
defp join(l, "", joiner) do
s = to_string l
if s |> String.contains?(joiner) do
s
else
String.to_atom s
end
end
defp join("", r, joiner) do
join(r, "", joiner)
end
defp join(l, r, joiner) do
to_string to_string(l) <> joiner <> to_string(r)
end
##############################################################################
@lint [{Credo.Check.Refactor.Nesting, false}, {Credo.Check.Refactor.ABCSize, false}]
defp put_or_update(input, joiner, prefix, value, fun, path \\ "") when is_map(input) do
case prefix |> to_string |> String.split(joiner, parts: 2) do
[key, rest] ->
{_, target} = input |> Map.get_and_update(join(key), fn current ->
old = case current do
nil -> %{}
_ -> current
end
{current, old |> put_or_update(joiner, rest, value, fun, join(path, key, joiner))}
end)
target
[key] ->
unless is_nil(fun), do: fun.({path, value})
cond do
is_map(input) -> input |> Map.put(join(key), value)
is_list(input) -> input ++ [value]
true -> input # FIXME raise ??
end
end
end
##############################################################################
defp parse_key(key, joiner, prefix) do
k = key
|> to_string
|> String.split(joiner)
|> Enum.at((prefix |> String.split(joiner) |> Enum.count) - 1)
try do
k |> String.to_integer
rescue
ArgumentError -> k
end
end
defp filter_keys(input, prefix) do
case prefix do
"" -> input
_ -> input |> Enum.filter(fn e -> e |> to_string |> String.starts_with?(to_string(prefix)) end)
end
end
defp quacks_as_list(input, joiner, prefix \\ "") do
input = input |> Map.keys |> filter_keys(prefix)
(input
|> Enum.map(fn k ->
k |> parse_key(joiner, prefix)
end)
|> Enum.sort) == (0..Enum.count(input) - 1 |> Enum.to_list)
end
@lint [{Credo.Check.Refactor.Nesting, false}]
defp is_struct(input) when is_map(input) do
input |> Enum.reduce(nil, fn {k, _}, acc ->
case k |> to_string |> String.split(~r{#{@struct_joiner}(?=[^#{@struct_joiner}]*$)}) do
[^acc, _] -> acc
[struct_name, _] -> if acc == nil, do: struct_name, else: false
_ -> false
end
end)
end
@lint [{Credo.Check.Refactor.ABCSize, false}]
defp imply_lists(input, joiner) when is_map(input) do
if quacks_as_list(input, joiner) do
sorted = input
|> Enum.sort(fn ({k1, _}, {k2, _}) ->
String.to_integer(to_string(k1)) < String.to_integer(to_string(k2))
end)
for {_, v} <- sorted do
if is_map(v), do: imply_lists(v, joiner), else: v
end
else
target = case is_struct(input) do
false -> %{}
name -> struct(Module.concat(Elixir, name |> to_string |> String.replace(@struct_joiner, ".")))
end
input |> Enum.reduce(target, fn {k, v}, acc ->
key = join(~r{.*#{@struct_joiner}} |> Regex.replace(to_string(k), ""))
acc |> Map.put(join(key), (if is_map(v), do: imply_lists(v, joiner), else: v))
end)
end
end
end