Skip to content

Commit ef07c68

Browse files
author
Aleksei Matiushkin
committed
keys: :reverse configuration parameter
1 parent ea30c5c commit ef07c68

File tree

4 files changed

+70
-92
lines changed

4 files changed

+70
-92
lines changed

.credo.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
## Refactoring Opportunities
118118
#
119119
{Credo.Check.Refactor.CondStatements, []},
120-
{Credo.Check.Refactor.CyclomaticComplexity, []},
120+
{Credo.Check.Refactor.CyclomaticComplexity, [max_complexity: 22]},
121121
{Credo.Check.Refactor.FunctionArity, []},
122122
{Credo.Check.Refactor.LongQuoteBlocks, []},
123123
# {Credo.Check.Refactor.MapInto, []},

lib/iteraptor.ex

+54-90
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,23 @@ defmodule Iteraptor do
9191
%{"a_b_c" => 42, "a_b_d_0" => nil, "a_b_d_1" => 42, "a_e_0" => :f, "a_e_1" => 42}
9292
"""
9393

94-
@spec to_flatmap(%{} | [...], Keyword.t()) :: %{}
94+
@type option ::
95+
{:keys, :reverse}
96+
| {:yield, :all | :none | :maps | :lists}
97+
| {:structs, :values | :keep}
98+
@type options :: [option()]
99+
100+
@typedoc """
101+
The function that might be passed to all the traversion functions.
102+
103+
When it’s a function or arity `1`, it receives `{key, value}` tuple when the key
104+
is the list of keys down the nesting levels.
105+
106+
When its arity is `2`, it receives `key` and `value` as separated arguments.
107+
"""
108+
@type traverse_fun :: ({any(), any()} -> any()) | (any(), any() -> any())
109+
110+
@spec to_flatmap(Access.t(), options()) :: %{}
95111

96112
def to_flatmap(input, opts \\ []) when is_map(input) or is_list(input) do
97113
reducer = fn {k, v}, acc ->
@@ -155,25 +171,22 @@ defmodule Iteraptor do
155171
{[0, :b], 42}
156172
[%{a: 42, b: 42}]
157173
"""
158-
@spec from_flatmap(%{}, ({any(), any()} -> any()) | nil, Keyword.t()) ::
159-
%{} | [...] | Keyword.t()
174+
@spec from_flatmap(%{}, traverse_fun(), options()) :: Access.t()
160175

161-
def from_flatmap(input, transformer \\ nil, opts \\ []) when is_map(input) do
176+
def from_flatmap(input, transformer \\ & &1, opts \\ []) when is_map(input) do
162177
reducer = fn {k, v}, acc ->
163178
key =
164179
case k |> Enum.join(delimiter(opts)) |> String.split(delimiter(opts)) do
165180
[k] -> [smart_convert(k)]
166181
list -> Enum.map(list, &smart_convert/1)
167182
end
168183

184+
transformer_key = if opts[:keys] == :reverse, do: Enum.reverse(key), else: key
185+
169186
value =
170-
if is_nil(transformer) do
171-
v
172-
else
173-
case transformer.({key, v}) do
174-
{^key, any} -> any
175-
any -> any
176-
end
187+
case transformer.({transformer_key, v}) do
188+
{^key, any} -> any
189+
any -> any
177190
end
178191

179192
deep_put_in(acc, {key, value}, opts)
@@ -198,9 +211,10 @@ defmodule Iteraptor do
198211
is an array or deeply nested keys;
199212
e.g. on `%{a: {b: 42}}` will be called once, with tuple `{[:a, :b], 42}`;
200213
- `opts`: the options to be passed to the iteration
201-
- `yield`: `[:all | :maps | :keywords | nil]` what to yield; _default:_ `nil`
214+
- `yield`: `[:all | :none | :maps | :lists]` what to yield; _default:_ `:all`
202215
for yielding _values only_
203-
- `structs`: `[:values | :keep | nil]` how to handle structs; _default:_ `nil`
216+
- `keys`: `[:reverse]` reverse keys list to ease pattern matching; _default:_ `nil`
217+
- `structs`: `[:values | :keep]` how to handle structs; _default:_ `nil`
204218
for treating them as `map`s. When `:values`, the nested structs
205219
are considered leaves and returned to the iterator instead of being iterated
206220
through; when `:keep` it returns a struct back after iteration
@@ -218,8 +232,7 @@ defmodule Iteraptor do
218232
%{a: %{b: %{c: 42}}}
219233
"""
220234

221-
@spec each(%{} | Keyword.t() | [...] | Access.t(), ({any(), any()} -> any()), Keyword.t()) ::
222-
%{} | Keyword.t() | [...] | Access.t()
235+
@spec each(Access.t(), traverse_fun(), options()) :: Access.t()
223236

224237
def each(input, fun, opts \\ []) do
225238
map(input, fun, opts)
@@ -239,9 +252,7 @@ defmodule Iteraptor do
239252
- `fun`: callback to be called on each **`{key, value}`** pair, where `key`
240253
is an array or deeply nested keys;
241254
e.g. on `%{a: {b: 42}}` will be called once, with tuple `{[:a, :b], 42}`;
242-
- `opts`: the options to be passed to the iteration
243-
- `yield`: `[:all | :maps | :keywords |` what to yield; _default:_ `nil`
244-
for yielding _values only_.
255+
- `opts`: the options to be passed to the iteration (see `Iteraptpr.each/3`)
245256
246257
## Examples
247258
@@ -259,8 +270,7 @@ defmodule Iteraptor do
259270
%{a: %{b: "YAY"}}
260271
"""
261272

262-
@spec map(%{} | Keyword.t() | [...] | Access.t(), ({any(), any()} -> any()), Keyword.t()) ::
263-
%{} | Keyword.t() | [...] | Access.t()
273+
@spec map(Access.t(), traverse_fun(), options()) :: Access.t()
264274

265275
def map(input, fun, opts \\ []) do
266276
unless is_function(fun, 1), do: raise("Function or arity fun/1 is required")
@@ -285,9 +295,7 @@ defmodule Iteraptor do
285295
- `fun`: callback to be called on each **`{key, value}, acc`** pair,
286296
where `key` is an array or deeply nested keys, `value` is the value and
287297
`acc` is the accumulator;
288-
- `opts`: the options to be passed to the iteration
289-
- `yield`: `[:all | :maps | :keywords |` what to yield; _default:_ `nil`
290-
for yielding _values only_.
298+
- `opts`: the options to be passed to the iteration (see `Iteraptpr.each/3`)
291299
292300
## Examples
293301
@@ -299,12 +307,7 @@ defmodule Iteraptor do
299307
["a", "a_b", "a_b_c"]
300308
"""
301309

302-
@spec reduce(
303-
%{} | Keyword.t() | [...] | Access.t(),
304-
%{} | Keyword.t() | [...] | Access.t(),
305-
({any(), any()}, any() -> any()),
306-
Keyword.t()
307-
) :: {%{} | Keyword.t() | [...] | Access.t(), any()}
310+
@spec reduce(Access.t(), Access.t(), traverse_fun(), options()) :: {Access.t(), any()}
308311

309312
def reduce(input, acc \\ nil, fun, opts \\ []) do
310313
unless is_function(fun, 2), do: raise("Function or arity fun/2 is required")
@@ -332,9 +335,7 @@ defmodule Iteraptor do
332335
- `fun`: callback to be called on each **`{key, value}, acc`** pair,
333336
where `key` is an array or deeply nested keys, `value` is the value and
334337
`acc` is the accumulator;
335-
- `opts`: the options to be passed to the iteration
336-
- `yield`: `[:all | :maps | :keywords |` what to yield; _default:_ `nil`
337-
for yielding _values only_.
338+
- `opts`: the options to be passed to the iteration (see `Iteraptpr.each/3`)
338339
339340
## Examples
340341
@@ -346,12 +347,7 @@ defmodule Iteraptor do
346347
{%{a: %{b: %{c: 84}}}, ["a.b.c=", "a.b", "a"]}
347348
"""
348349

349-
@spec map_reduce(
350-
%{} | Keyword.t() | [...] | Access.t(),
351-
%{} | Keyword.t() | [...] | Access.t(),
352-
({any(), any()}, any() -> any()),
353-
Keyword.t()
354-
) :: {%{} | Keyword.t() | [...] | Access.t(), any()}
350+
@spec map_reduce(Access.t(), Access.t(), traverse_fun(), options()) :: {Access.t(), any()}
355351

356352
def map_reduce(input, acc \\ %{}, fun, opts \\ []) do
357353
unless is_function(fun, 2), do: raise("Function or arity fun/2 is required")
@@ -378,9 +374,7 @@ defmodule Iteraptor do
378374
379375
- `input`: nested map/list/keyword to be filtered.
380376
- `fun`: callback to be called on each **`{key, value}`** to filter entries.
381-
- `opts`: the options to be passed to the iteration
382-
- `yield`: `[:all | :maps | :keywords |` what to yield; _default:_ `nil`
383-
for yielding _values only_.
377+
- `opts`: the options to be passed to the iteration (see `Iteraptpr.each/3`)
384378
385379
## Examples
386380
@@ -389,8 +383,7 @@ defmodule Iteraptor do
389383
%{a: %{e: %{c: 42}, d: %{c: 42}}, c: 42}
390384
"""
391385

392-
@spec filter(%{} | Keyword.t() | [...] | Access.t(), ({any(), any()} -> any()), Keyword.t()) ::
393-
{%{} | Keyword.t() | [...] | Access.t(), any()}
386+
@spec filter(Access.t(), traverse_fun(), options()) :: {Access.t(), any()}
394387

395388
def filter(input, fun, opts \\ []) do
396389
unless is_function(fun, 1), do: raise("Function or arity fun/1 is required")
@@ -427,7 +420,7 @@ defmodule Iteraptor do
427420
iex> Iteraptor.jsonify([foo: [bar: [baz: :zoo], boo: 42]], keys: false)
428421
%{foo: %{bar: %{baz: :zoo}, boo: 42}}
429422
"""
430-
@spec jsonify(Access.container() | any(), opts :: list()) :: map()
423+
@spec jsonify(Access.container() | any(), keyword()) :: map()
431424
def jsonify(input, opts \\ [])
432425
def jsonify([{_, _} | _] = input, opts), do: input |> Map.new() |> jsonify(opts)
433426
def jsonify(input, opts) when is_list(input), do: Enum.map(input, &jsonify(&1, opts))
@@ -458,15 +451,18 @@ defmodule Iteraptor do
458451

459452
##############################################################################
460453

461-
@spec traverse_callback(({any(), any()} -> any()) | (any(), any() -> any()), {any(), any()}) ::
462-
{any(), any()}
454+
@spec traverse_callback(nil | traverse_fun(), {any(), any()}, nil | :reverse) :: {any(), any()}
463455

464-
defp traverse_callback(fun, {value, acc}) do
465-
case fun do
466-
f when is_function(fun, 1) -> {f.(value), nil}
467-
f when is_function(fun, 2) -> f.(value, acc)
468-
end
469-
end
456+
defp traverse_callback(nil, {value, acc}, _), do: {value, acc}
457+
458+
defp traverse_callback(fun, {{keys, value}, acc}, :reverse),
459+
do: traverse_callback(fun, {{Enum.reverse(keys), value}, acc}, nil)
460+
461+
defp traverse_callback(fun, {value, acc}, nil) when is_function(fun, 1),
462+
do: {fun.(value), acc}
463+
464+
defp traverse_callback(fun, {value, acc}, nil) when is_function(fun, 2),
465+
do: fun.(value, acc)
470466

471467
defmacrop traverse_value({k, v}, fun, opts, {deep, acc}) do
472468
quote do
@@ -475,16 +471,10 @@ defmodule Iteraptor do
475471
end
476472
end
477473

478-
@spec traverse(
479-
%{} | Keyword.t() | [...] | Access.t(),
480-
({any(), any()} -> any()) | (any(), any() -> any()),
481-
Keyword.t(),
482-
{[any()], any()}
483-
) :: {%{} | Keyword.t() | [...] | Access.t(), any()}
474+
@spec traverse(Access.t(), traverse_fun(), options(), {[any()], any()}) :: {Access.t(), any()}
484475

485476
defp traverse(input, fun, opts, key_acc)
486477

487-
# credo:disable-for-lines:41
488478
defp traverse(input, fun, opts, {key, acc}) when is_list(input) or is_map(input) do
489479
{type, from, into} = type(input)
490480

@@ -507,10 +497,11 @@ defmodule Iteraptor do
507497

508498
{value, acc} =
509499
case {opts[:yield], is_map(v) and not s_as_v, is_list(v)} do
510-
{_, false, false} -> traverse_callback(fun, {{deep, v}, acc})
511-
{:all, _, _} -> traverse_callback(fun, {{deep, v}, acc})
512-
{:lists, _, true} -> traverse_callback(fun, {{deep, v}, acc})
513-
{:maps, true, _} -> traverse_callback(fun, {{deep, v}, acc})
500+
{_, false, false} -> traverse_callback(fun, {{deep, v}, acc}, opts[:keys])
501+
{:all, _, _} -> traverse_callback(fun, {{deep, v}, acc}, opts[:keys])
502+
{:none, _, _} -> traverse_callback(nil, {{deep, v}, acc}, opts[:keys])
503+
{:lists, _, true} -> traverse_callback(fun, {{deep, v}, acc}, opts[:keys])
504+
{:maps, true, _} -> traverse_callback(fun, {{deep, v}, acc}, opts[:keys])
514505
_ -> {{deep, v}, acc}
515506
end
516507

@@ -535,31 +526,4 @@ defmodule Iteraptor do
535526
end
536527

537528
defp traverse(input, _fun, _opts, {_key, acc}), do: {input, acc}
538-
539-
# defp process(input, :struct, {acc, key, fun}, opts) do
540-
# struct_name = input.__struct__ |> inspect |> String.replace(".", struct_joiner(opts))
541-
# input
542-
# |> Map.keys
543-
# |> Enum.reject(& &1 == :__struct__)
544-
# |> Enum.map(fn e ->
545-
# {"#{struct_name}%#{e}", get_in(input, [Access.key!(e)])}
546-
# end)
547-
# |> Enum.into(into(opts))
548-
# |> process(Map, {acc, key, fun}, opts)
549-
# end
550-
551-
##############################################################################
552-
553-
##############################################################################
554-
555-
# defp is_struct(input) when is_map(input) do
556-
# input |> Enum.reduce(nil, fn {k, _}, acc ->
557-
# case k |> to_string |> String.split(~r{#{@struct_joiner}(?=[^#{@struct_joiner}]*$)}) do
558-
# [^acc, _] -> acc
559-
# [struct_name, _] -> if acc == nil, do: struct_name, else: false
560-
# _ -> false
561-
# end
562-
# end)
563-
# end
564-
# defp is_struct(_), do: false
565529
end

mix.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
%{
22
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
3-
"credo": {:hex, :credo, "1.5.3", "f345253655f2efe1e4693a03437606462681e91303ebc9e3909c14268effc37a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f7e238c10051cc22515e3f75754200b567d93c00d93be81fc59d47bc3dfdc5be"},
3+
"credo": {:hex, :credo, "1.5.4", "9914180105b438e378e94a844ec3a5088ae5875626fc945b7c1462b41afc3198", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cf51af45eadc0a3f39ba13b56fdac415c91b34f7b7533a13dc13550277141bc4"},
44
"dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"},
55
"earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"},
66
"earmark_parser": {:hex, :earmark_parser, "1.4.12", "b245e875ec0a311a342320da0551da407d9d2b65d98f7a9597ae078615af3449", [:mix], [], "hexpm", "711e2cc4d64abb7d566d43f54b78f7dc129308a63bc103fbd88550d2174b3160"},

test/iteraptor_test.exs

+14
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,20 @@ defmodule Iteraptor.Test do
7777
assert result =~ "{[:a2, :a4, :a6, :a8], 42}"
7878
end
7979

80+
test "keyword / each prints keys in the reversed order" do
81+
result =
82+
capture_log(fn ->
83+
@keyword |> Iteraptor.each(fn {k, v} -> Logger.debug(inspect({k, v})) end, keys: :reverse)
84+
end)
85+
86+
assert result =~ "{[:a1], 42}"
87+
assert result =~ "{[:a3, :a2], 42}"
88+
assert result =~ "{[:a5, :a4, :a2], 42}"
89+
assert result =~ "{[:a7, :a6, :a4, :a2], 42}"
90+
assert result =~ "{[:a7, :a6, :a4, :a2], 3.14}"
91+
assert result =~ "{[:a8, :a6, :a4, :a2], 42}"
92+
end
93+
8094
test "Iteraptor.jsonify/2 works" do
8195
result = Iteraptor.jsonify(@keyword)
8296

0 commit comments

Comments
 (0)