Packages
jsv
0.13.0
0.21.2
0.21.1
0.21.0
0.20.0
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.3
0.18.2
0.18.1
0.18.0
0.17.1
0.17.0
0.16.0
0.15.2
0.15.1
0.15.0
0.14.0
0.13.1
0.13.0
0.12.0
0.11.5
0.11.4
0.11.3
retired
0.11.2
0.11.1
0.11.0
0.10.1
0.10.0
0.9.0
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.0
0.5.1
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
A JSON Schema Validator with complete support for the latest specifications.
Current section
Files
Jump to
Current section
Files
lib/jsv/helpers/traverse.ex
defmodule JSV.Helpers.Traverse do
@moduledoc """
Helpers to work with nested generic data structures.
"""
@type postwalk_cont :: (map, term -> {map, term})
@type postwalk_item :: {:key, term} | {:val, term} | {:struct, struct, postwalk_cont}
@type prewalk_item :: {:key, term} | {:val, term} | {:struct, struct}
@doc """
Updates a data structure in depth-first, post-order traversal.
Operates like `postwalk/3` but without an accumulator. Handling continuations
for structs require to handle the accumulator, whose value MUST be `nil`.
"""
@spec postwalk(data, (postwalk_item() -> data)) :: data when data: term
def postwalk(data, fun) when is_function(fun, 1) do
{value, _} = postwalk(data, nil, fn value, nil -> {fun.(value), nil} end)
value
end
@doc """
Updates a JSON-compatible data structure in depth-first, post-order traversal
while carrying an accumulator.
The callback must return a `{new_value, new_acc}` tuple.
Nested data structures are given to the callback before their wrappers, and
when the wrappers are called, their children are already updated.
JSON-compatible only means that there are restrictions on map keys and struct
values:
* The callback function will be called for any key but will not traverse the
keys. For instance, with data such as `%{{x, y} => "some city"}`, the tuple
used as key will be passed as-is but the callback will not be called for
individual tuple elements.
* Structs will be passed as `{:struct, value, continuation}`. The struct keys
and values will **NOT** have been traversed yet. The callback should extract
the struct into another term (for instance using `Map.from_struct/1`) and
call the continuation.
To normalize this new term you MUST call the conitnuation function manually.
To respect the post-order of traversal, it SHOULD be called before further
transformation of the struct:
Traverse.postwalk(%MyStruct, [], fn
{:struct, my_struct, cont}, acc ->
{map, acc} = cont.(Map.from_struct(my_struct), acc)
{struct!(MyStruct, do_something_with_map(map)), acc}
{:val, ...} -> ...
end)
Note that if a map is given to the continuation function, the map itself
will not be passed to your callback as `{:val, map}`, each key and value
will be normalized directly.
Since key normalization does not support structs, the keys must be already
normalized and JSON-encodable when giving a map to the continuation
function.
* General data is accepted: tuples, pid, refs, etc. *
"""
@spec postwalk(data, acc, (postwalk_item, acc -> {data, acc})) :: {data, acc} when data: term, acc: term
def postwalk(data, acc, fun) when is_function(fun, 2) do
{data, acc} = postwalk_subs(data, acc, fun)
postwalk_parent(data, acc, fun)
rescue
e in ArgumentError ->
stack =
case __STACKTRACE__ do
[h | t] -> [h | filter_stacktrace(t)]
other -> other
end
reraise e, stack
end
# Remove the traverse numerous stacktraces except the topmost
defp filter_stacktrace(stack) do
not_traverse? = &(elem(&1, 0) != __MODULE__)
{zyx, cba} = Enum.split_while(stack, not_traverse?)
cba =
cba
# invert the stack to be top → bottom
|> :lists.reverse()
# take the top of the stack that is not traverse mod
|> Enum.split_while(not_traverse?)
# then reverse it to go back to bottom → top order
# also keep one traverse call if any
|> case do
{ab, [c | _]} -> [c | :lists.reverse(ab)]
{ab, []} -> :lists.reverse(ab)
end
zyx ++ cba
end
defp postwalk_parent(struct, acc, fun) when is_struct(struct) do
cont = fn
value, _acc when is_struct(value) ->
raise ArgumentError, "continuation function does not accept structs, got: #{inspect(value)}"
value, acc when is_map(value) ->
postwalk_map_pairs(value, acc, fun)
value, acc ->
postwalk(value, acc, fun)
end
fun.({:struct, struct, cont}, acc)
end
defp postwalk_parent(data, acc, fun) do
fun.({:val, data}, acc)
end
defp postwalk_subs(struct, acc, _fun) when is_struct(struct) do
# Iteration on the struct is made in the continuation function
{struct, acc}
end
defp postwalk_subs(map, acc, fun) when is_map(map) do
postwalk_map_pairs(map, acc, fun)
end
defp postwalk_subs(list, acc, fun) when is_list(list) do
Enum.map_reduce(list, acc, fn item, acc -> {_v, _acc} = postwalk(item, acc, fun) end)
end
# small optimization for 2-tuples
defp postwalk_subs({a, b}, acc, fun) do
{a, acc} = postwalk(a, acc, fun)
{b, acc} = postwalk(b, acc, fun)
{{a, b}, acc}
end
defp postwalk_subs(tuple, acc, fun) when is_tuple(tuple) do
{elems, acc} =
tuple
|> Tuple.to_list()
|> Enum.map_reduce(acc, fn item, acc -> {_v, _acc} = postwalk(item, acc, fun) end)
{List.to_tuple(elems), acc}
end
defp postwalk_subs(data, acc, _fun) do
{data, acc}
end
defp postwalk_map_pairs(map, acc, fun) do
{pairs, acc} =
Enum.map_reduce(map, acc, fn {k, v}, acc ->
{v, acc} = postwalk(v, acc, fun)
{k, acc} = fun.({:key, k}, acc)
{{k, v}, acc}
end)
{Map.new(pairs), acc}
end
@doc """
Updates a data structure in depth-first, pre-order traversal.
Operates like `prewalk/3` but without an accumulator.
"""
@spec prewalk(data, (prewalk_item() -> data)) :: data when data: term
def prewalk(data, fun) when is_function(fun, 1) do
{value, _} = prewalk(data, nil, fn value, nil -> {fun.(value), nil} end)
value
end
@doc """
Updates a JSON-compatible data structure in depth-first, pre-order traversal
while carrying an accumulator.
The callback must return a `{new_value, new_acc}` tuple.
Nested data structures are given iterated after the parent data has been given
to the function. So it is possible to accept a container (map, list, tuple)
and return another one from the callback before the children are iterated.
JSON-compatible only means that there are restrictions on map keys and struct
values:
* The callback function will be called for any key but will not traverse the
keys. For instance, with data such as `%{{x, y} => "some city"}`, the tuple
used as key will be passed as-is but the callback will not be called for
individual tuple elements.
* Structs are passed as a `{:struct, struct}` tuple.
* General data is accepted: tuples, pid, refs, etc. *
"""
@spec prewalk(data, acc, (prewalk_item, acc -> {data, acc})) :: {data, acc} when data: term, acc: term
def prewalk(data, acc, fun) when is_function(fun, 2) do
{new_value, acc} = prewalk_parent(data, acc, fun)
prewalk_subs(new_value, acc, fun)
end
defp prewalk_parent(struct, acc, fun) when is_struct(struct) do
fun.({:struct, struct}, acc)
end
defp prewalk_parent(val, acc, fun) do
fun.({:val, val}, acc)
end
# TODO(doc) structs keys are not traversed
defp prewalk_subs(%mod{} = struct, acc, fun) do
# we need to preserve the struct, that means keep its keys.
{pairs, acc} =
struct
|> Map.from_struct()
|> Enum.map_reduce(acc, fn {k, v}, acc ->
{v, acc} = prewalk(v, acc, fun)
{{k, v}, acc}
end)
{struct(mod, pairs), acc}
end
defp prewalk_subs(map, acc, fun) when is_map(map) do
prewalk_map_pairs(map, acc, fun)
end
defp prewalk_subs(list, acc, fun) when is_list(list) do
Enum.map_reduce(list, acc, fn item, acc -> prewalk(item, acc, fun) end)
end
defp prewalk_subs({a, b}, acc, fun) do
{a, acc} = prewalk(a, acc, fun)
{b, acc} = prewalk(b, acc, fun)
{{a, b}, acc}
end
defp prewalk_subs(tuple, acc, fun) when is_tuple(tuple) do
{elems, acc} =
tuple
|> Tuple.to_list()
|> Enum.map_reduce(acc, fn item, acc -> {_v, _acc} = prewalk(item, acc, fun) end)
{List.to_tuple(elems), acc}
end
defp prewalk_subs(scalar, acc, _fun) do
{scalar, acc}
end
defp prewalk_map_pairs(map, acc, fun) do
{pairs, acc} =
Enum.map_reduce(map, acc, fn {k, v}, acc ->
{v, acc} = prewalk(v, acc, fun)
{k, acc} = fun.({:key, k}, acc)
{{k, v}, acc}
end)
{Map.new(pairs), acc}
end
end