Current section

Files

Jump to
odgn_json_pointer lib json_pointer.ex
Raw

lib/json_pointer.ex

defmodule JSONPointer do
@moduledoc """
An implementation of [JSON Pointer (RFC 6901)](http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-08) for Elixir.
"""
import JSONPointer.Guards
import JSONPointer.Serialize
import JSONPointer.Utils
@typedoc """
the item on which the operations are applied
"""
@type container :: map | list
@typedoc """
the JSON Pointer value
"""
@type pointer :: String.t() | [String.t()]
@typedoc """
return type
"""
@type t :: nil | true | false | list | float | integer | String.t() | map
@typedoc """
data that was affected by an operation
"""
@type existing :: t
@typedoc """
data that was removed by an operation
"""
@type removed :: t
@typedoc """
a tuple of JSON Pointer and value
"""
@type pointer_list :: {String.t(), t}
@typedoc """
return error tuple
"""
@type error_message :: {:error, String.t()}
@typep transform_fn :: (any() -> any())
@typedoc """
argument passed to transform/2 mapping a pointer using a function or
another pointer
"""
@type transform_mapping ::
{pointer, pointer}
| {pointer, pointer, transform_fn}
| {pointer, (() -> any())}
@typep strict :: boolean
@typedoc """
options that may be passed to functions to control outcomes
"""
@type options :: %{
optional(:strict) => strict
}
@default_options %{:strict => false}
@default_add_options %{:strict => true}
@doc """
Retrieves the value indicated by the pointer from the object
## Examples
iex> JSONPointer.get( %{ "fridge" => %{ "door" => "milk" } }, "/fridge/door" )
{:ok, "milk"}
iex> JSONPointer.get( %{ "contents" => [ "milk", "butter", "eggs" ]}, "/contents/2" )
{:ok, "eggs"}
iex> JSONPointer.get( %{ "milk" => true, "butter" => false}, "/cornflakes" )
{:error, "token not found: cornflakes"}
iex> JSONPointer.get( %{ "contents" => [ "milk", "butter", "eggs" ]}, "/contents/4" )
{:error, "index out of bounds: 4"}
"""
@spec get(container, pointer) :: {:ok, t} | error_message
def get(obj, pointer, options \\ @default_options) do
case walk_container(:get, obj, pointer, nil, options) do
{:ok, value, _} -> {:ok, value}
{:error, msg, _} -> {:error, msg}
end
end
@doc """
Retrieves the value indicated by the pointer from the object, raises
an error on exception
## Examples
iex> JSONPointer.get!( %{ "fridge" => %{ "milk" => true}}, "/fridge/milk" )
true
iex> JSONPointer.get!( %{}, "/fridge/milk" )
** (ArgumentError) json pointer key not found: fridge
"""
@spec get!(container, pointer, options) :: t | no_return
def get!(obj, pointer, options \\ @default_options) do
case walk_container(:get, obj, pointer, nil, options) do
{:ok, value, _} -> value
{:error, msg, _} -> raise ArgumentError, message: msg
end
end
@doc """
Returns true if the given JSON Pointer resolves to a value
## Examples
iex> JSONPointer.has?( %{ "milk" => true, "butter" => false}, "/butter" )
true
iex> JSONPointer.has?( %{ "milk" => true, "butter" => false}, "/cornflakes" )
false
"""
@spec has?(container, pointer, options) :: boolean
def has?(obj, pointer, options \\ @default_options) do
case walk_container(:has, obj, pointer, nil, options) do
{:ok, _obj, _existing} -> true
{:error, _, _} -> false
end
end
@doc """
Tests whether a JSON Pointer equals the given value
## Examples
iex> JSONPointer.test( %{ "milk" => "skimmed", "butter" => false}, "/milk", "skimmed" )
{:ok, %{ "milk" => "skimmed", "butter" => false} }
"""
@spec test(container, pointer, t, options) :: {:ok, t} | error_message
def test(obj, pointer, value, options \\ @default_options) do
case walk_container(:get, obj, pointer, nil, options) do
{:ok, result, _} ->
case are_equal?(value, result) do
{:ok, _} -> {:ok, obj}
{:error, msg} -> {:error, msg}
end
{:error, msg, _} ->
{:error, msg}
end
end
@doc """
Tests whether a JSON Pointer equals the given value, raises an error if they don't
## Examples
iex> JSONPointer.test!( %{ "milk" => "skimmed", "butter" => false}, "/butter", "unsalted" )
** (ArgumentError) not equal
"""
@spec test!(container, pointer, t, options) :: t | no_return
def test!(obj, pointer, value, options \\ @default_options) do
case test(obj, pointer, value, options) do
{:ok, result} -> result
{:error, msg} -> raise ArgumentError, message: msg
end
end
@doc """
Removes an attribute of object referenced by pointer
## Examples
iex> JSONPointer.remove( %{"fridge" => %{ "milk" => true, "butter" => true}}, "/fridge/butter" )
{:ok, %{"fridge" => %{"milk"=>true}}, true }
iex> JSONPointer.remove( %{"fridge" => %{ "milk" => true, "butter" => true}}, "/fridge/sandwich" )
{:error, "json pointer key not found: sandwich", %{ "butter" => true, "milk" => true}}
"""
@spec remove(container, pointer, options) :: {:ok, t, removed} | error_message
def remove(object, pointer, options \\ @default_options) do
walk_container(:remove, object, pointer, nil, options)
end
@doc """
Sets a new value on object at the location described by pointer
## Examples
iex> JSONPointer.set( %{}, "/example/msg", "hello")
{:ok, %{ "example" => %{ "msg" => "hello" }}, nil }
iex> JSONPointer.set( %{}, "/fridge/contents/1", "milk" )
{:ok, %{"fridge" => %{"contents" => [nil, "milk"]}}, nil }
iex> JSONPointer.set( %{"milk"=>"full"}, "/milk", "empty")
{:ok, %{"milk" => "empty"}, "full"}
"""
@spec set(container, pointer, t, options) :: {:ok, t, existing} | error_message
def set(obj, pointer, value, options \\ @default_options) do
case walk_container(:set, obj, pointer, value, options) do
{:ok, result, existing} -> {:ok, result, existing}
{:error, msg, _} -> {:error, msg}
end
end
@doc """
Sets a new value on object at the location described by pointer, raises
an error on exception
## Examples
iex> JSONPointer.set!( %{}, "/example/msg", "hello")
%{ "example" => %{ "msg" => "hello" }}
iex> JSONPointer.set!( %{}, "/fridge/contents/1", "milk" )
%{"fridge" => %{"contents" => [nil, "milk"]}}
iex> JSONPointer.set!( %{"milk"=>"full"}, "/milk", "empty")
%{"milk" => "empty"}
"""
@spec set!(container, pointer, t, options) :: t | no_return
def set!(obj, pointer, value, options \\ @default_options) do
case walk_container(:set, obj, pointer, value, options) do
{:ok, result, _existing} -> result
{:error, msg, _} -> raise ArgumentError, message: msg
end
end
@doc """
Adds a value using the given pointer
Follows the JSON patch behaviour specified in rfc6902
## Examples
iex> JSONPointer.add( %{ "fridge" => [ "milk", "cheese" ] }, "/fridge/1", "salad")
{:ok, %{ "fridge" => [ "milk", "salad", "cheese" ]}, [ "milk", "cheese" ] }
iex> JSONPointer.add( %{ "a" => %{"b" => %{}}}, "/a/b/c", ["foo", "bar"] )
{:ok, %{"a" => %{"b" => %{"c" => ["foo", "bar"]}}}, nil}
"""
@spec add(container, pointer, t) :: {:ok, t, existing} | error_message
def add(obj, pointer, value, options \\ @default_add_options) do
case walk_container(:add, obj, pointer, value, options) do
{:ok, result, existing} -> {:ok, result, existing}
{:error, msg, target} -> {:error, msg, target}
end
end
@doc """
Adds a value using the given pointer, raises an error on exception
## Examples
iex> JSONPointer.add!( %{ "a" => %{ "foo" => 1 } }, "/a/b", true )
%{"a" => %{"foo" => 1, "b" => true}}
"""
@spec add!(container, pointer, t) :: t | no_return
def add!(obj, pointer, value, options \\ @default_add_options) do
case add(obj, pointer, value, options) do
{:ok, result, _existing} -> result
{:error, msg, _} -> raise ArgumentError, message: msg
end
end
@doc """
Extracts a list of JSON pointer paths from the given object
## Examples
iex> JSONPointer.dehydrate( %{"a"=>%{"b"=>["c","d"]}} )
{:ok, [{"/a/b/0", "c"}, {"/a/b/1", "d"}] }
iex> JSONPointer.dehydrate( %{"a"=>[10, %{"b"=>12.5}], "c"=>99} )
{:ok, [{"/a/0", 10}, {"/a/1/b", 12.5}, {"/c", 99}] }
"""
@spec dehydrate(container) :: {:ok, pointer_list} | error_message
def dehydrate(object) do
{:ok, dehydrate_container(object, [], [])}
end
@doc """
Extracts a list of JSON pointer paths from the given object, raises
an error on exception
## Examples
iex> JSONPointer.dehydrate!( %{"a"=>%{"b"=>["c","d"]}} )
[{"/a/b/0", "c"}, {"/a/b/1", "d"}]
iex> JSONPointer.dehydrate!( %{"a"=>[10, %{"b"=>12.5}], "c"=>99} )
[{"/a/0", 10}, {"/a/1/b", 12.5}, {"/c", 99}]
"""
@spec dehydrate!(container) :: pointer_list
def dehydrate!(object) do
dehydrate_container(object, [], [])
end
@doc """
Applies the given list of paths to the given container
## Examples
iex> JSONPointer.hydrate( %{}, [ {"/a/b/1", "find"} ] )
{:ok, %{"a"=>%{"b"=>[nil,"find"]} } }
"""
@spec hydrate(container, pointer_list) :: {:ok, container} | error_message
def hydrate(obj, pointer_list) do
reduce_result =
Enum.reduce(pointer_list, obj, fn {path, value}, acc ->
case JSONPointer.set(acc, path, value) do
{:ok, result, _} -> result
{:error, reason} -> {:error, reason}
end
end)
case reduce_result do
{:error, reason} -> {:error, reason}
result -> {:ok, result}
end
end
@doc """
Applies the given list of paths to the given container, raises an exception
on error
## Examples
iex> JSONPointer.hydrate!( %{}, [ {"/a/b/1", "find"} ] )
%{"a"=>%{"b"=>[nil,"find"]} }
"""
@spec hydrate!(container, pointer_list) :: container | no_return
def hydrate!(obj, pointer_list) do
case hydrate(obj, pointer_list) do
{:ok, result} -> result
{:error, msg} -> raise ArgumentError, message: msg
end
end
@doc """
Returns the given list of paths applied to a container
## Examples
iex> JSONPointer.hydrate( [ {"/a/1/b", "single"} ] )
{:ok, %{"a" => %{"1" => %{"b" => "single"}}}}
"""
@spec hydrate(pointer_list) :: {:ok, container} | error_message
def hydrate(pointer_list) do
hydrate(%{}, pointer_list)
end
@doc """
Returns the given list of paths applied to a container, raises an exception
on error
## Examples
iex> JSONPointer.hydrate!( [ {"/a/b/1", "find"} ] )
%{"a"=>%{"b"=>[nil,"find"]} }
"""
@spec hydrate!(pointer_list) :: container | no_return
def hydrate!(pointer_list) do
case hydrate(pointer_list) do
{:ok, result} -> result
{:error, msg} -> raise ArgumentError, message: msg
end
end
@doc """
Merges the incoming dst object into src
## Examples
iex> JSONPointer.merge( %{"a"=>1}, %{"b"=>2} )
{:ok, %{"a"=>1,"b"=>2} }
iex> JSONPointer.merge( ["foo", "bar"], ["baz"] )
{:ok, ["baz", "bar"]}
"""
@spec merge(container, container) :: {:ok, container}
def merge(src, dst) do
{:ok, paths} = dehydrate(dst)
hydrate(src, paths)
end
@doc """
Merges the incoming dst object into src, raises
an error on exception
## Examples
iex> JSONPointer.merge!( %{"a"=>1}, %{"b"=>2} )
%{"a"=>1,"b"=>2}
iex> JSONPointer.merge!( ["foo", "bar"], ["baz"] )
["baz", "bar"]
"""
@spec merge!(container, container) :: container | no_return
def merge!(src, dst) do
case merge(src, dst) do
{:ok, result} -> result
{:error, msg} -> raise ArgumentError, message: msg
end
end
@doc """
Applies a mapping of source paths to destination paths in the result
The mapping can optionally include a function which transforms the source
value before it is applied to the result.
## Examples
iex> JSONPointer.transform( %{ "a"=>4, "b"=>%{ "c" => true }}, [ {"/b/c", "/valid"}, {"/a","/count", fn val -> val*2 end} ] )
{:ok, %{"count" => 8, "valid" => true}}
"""
@spec transform(map(), transform_mapping) :: map()
def transform(src, mapping) do
result =
Enum.reduce(mapping, %{}, fn
{dst_path, transform}, acc when is_function(transform) ->
set!(acc, dst_path, transform.())
{src_path, dst_path}, acc ->
set!(acc, dst_path, get!(src, src_path))
{src_path, dst_path, transform}, acc ->
set!(acc, dst_path, transform.(get!(src, src_path)))
end)
{:ok, result}
end
@doc """
Applies a mapping of source paths to destination paths in the result, raises an
error on exception
The mapping can optionally include a function which transforms the source
value before it is applied to the result.
## Examples
iex> JSONPointer.transform!( %{ "a"=>5, "b"=>%{ "is_valid" => true }}, [ {"/b/is_valid", "/valid"}, {"/a","/count", fn val -> val*2 end} ] )
%{"count" => 10, "valid" => true}
"""
@spec transform!(map(), transform_mapping) :: map()
def transform!(src, mapping) do
case transform(src, mapping) do
{:ok, result} -> result
{:error, msg} -> raise ArgumentError, message: msg
end
end
defp insert_into(list, index, val) when is_list(list) do
list |> ensure_list_size(index) |> List.insert_at(index, val)
end
defp remove_from(list, index) when is_list(list) do
list |> List.pop_at(index)
end
defp apply_into(list, "-", val) when is_list(list) do
list ++ [val]
end
# set the list at index to val
defp apply_into(list, index, val) when is_list(list) do
# ensure the list has the capacity for this index
list |> ensure_list_size(index + 1) |> List.replace_at(index, val)
end
# set the key to val within a map
defp apply_into(map, key, val) when is_map(map) do
Map.put(map, key, val)
end
# when an empty pointer has been provided and this is an add operation,
# replace the root
defp walk_container(:add, object, "", value, _options) do
{:ok, value, object}
end
# when an empty pointer has been provided, simply return the incoming object
defp walk_container(_operation, object, "", _value, _options) do
{:ok, object, nil}
end
defp walk_container(_operation, object, "#", _value, _options) do
{:ok, object, nil}
end
defp walk_container(_operation, object, _pointer, _value, _options) when is_bitstring(object) do
raise ArgumentError, message: "invalid object: #{object}"
end
# begins the descent into a container using the specified pointer
defp walk_container(operation, object, pointer, value, options) when is_list(pointer) do
[token | tokens] = pointer
walk_container(operation, nil, object, token, tokens, value, options)
end
# begins the descent into a container using the specified pointer
defp walk_container(operation, object, pointer, value, options) do
case parse(pointer) do
{:ok, tokens} ->
[token | tokens] = tokens
walk_container(operation, nil, object, token, tokens, value, options)
{:error, reason, value} ->
{:error, reason, value}
end
end
# leaf operation: remove from map
defp walk_container(operation, _parent, map, "**", tokens, _value, _options)
when is_remove_from_map(operation, map, tokens) do
{:ok, nil, map}
end
# leaf operation: remove from map
defp walk_container(operation, _parent, map, token, tokens, _value, _options)
when is_remove_from_map(operation, map, tokens) do
case Map.fetch(map, token) do
{:ok, existing} ->
{:ok, Map.delete(map, token), existing}
:error ->
{:error, "json pointer key not found: #{token}", map}
end
end
# leaf operation: remove from list
defp walk_container(operation, _parent, list, token, tokens, _value, _options)
when is_remove_from_list(operation, list, tokens) do
case parse_index(token) do
{:error, msg} ->
{:error, msg, list}
index ->
{removed, list} = remove_from(list, index)
{:ok, list, removed}
end
end
# leaf operation: set token to value on a map
defp walk_container(operation, _parent, map, "-", tokens, value, _options)
when is_set_map(operation, map, tokens) do
{:ok, apply_into(map, "-", value), nil}
end
# leaf operation: set token to value on an empty map
defp walk_container(operation, _parent, map, token, tokens, value, _options)
when is_add_set_empty_map(operation, map, tokens) do
# IO.puts("empty map #{token}")
# IO.inspect(map)
case parse_index(token) do
{:error, _msg} ->
# this is ok, treat the token just as a string
{:ok, apply_into(map, token, value), nil}
index ->
# the token turned out to be an array index, so convert the value into a list
{:ok, apply_into([], index, value), nil}
end
end
# leaf operation: set token to value on a map
defp walk_container(operation, _parent, map, token, tokens, value, _options)
when is_add_set_map(operation, map, tokens) do
# IO.puts("uhh #{token}")
# IO.inspect(map)
case Map.fetch(map, token) do
{:ok, existing} ->
{:ok, apply_into(map, token, value), existing}
:error ->
{:ok, apply_into(map, token, value), nil}
end
end
# leaf operation: set wildcard to value on a list
defp walk_container(operation, _parent, list, "**", tokens, value, _options)
when is_set_list(operation, list, tokens) do
# replace each entry in the list with the value
result =
Enum.reduce(list, [], fn _entry, acc ->
acc ++ [value]
end)
{:ok, result, nil}
end
defp walk_container(operation, parent, _map, "**", tokens, value, _options)
when is_set_map(operation, parent, tokens) do
{:ok, value, nil}
end
defp walk_container(operation, _parent, list, "-", tokens, value, _options)
when is_add_set_list(operation, list, tokens) do
{:ok, apply_into(list, "-", value), nil}
end
defp walk_container(operation, _parent, list, token, tokens, value, options)
when is_add_list(operation, list, tokens) do
is_strict = Map.get(options, :strict)
case parse_index(token) do
{:error, msg} ->
# {:ok, apply_into( %{}, token, value), nil}
{:error, msg, list}
index ->
if is_strict && (index < 0 || index > Enum.count(list)) do
{:error, "index out of bounds: #{index}", list}
else
# Enum.at(list, index)}
{:ok, insert_into(list, index, value), list}
end
end
end
# leaf operation: set token(index) to value on a list
defp walk_container(operation, _parent, list, token, tokens, value, _options)
when is_set_list(operation, list, tokens) do
case parse_index(token) do
{:error, msg} ->
{:error, msg, list}
index ->
{:ok, apply_into(list, index, value), Enum.at(list, index)}
end
end
defp walk_container(operation, parent, list, "-", tokens, value, _options)
when is_set_list_nil_child(operation, parent, tokens, list) do
{:ok, apply_into([], "-", value), nil}
end
# leaf operation: no value for list, so we determine the container depending on the token
defp walk_container(operation, parent, list, token, tokens, value, _options)
when is_set_list_nil_child(operation, parent, tokens, list) do
case parse_index(token) do
{:error, _msg} ->
# this is fine, token is a string
{:ok, apply_into(%{}, token, value), nil}
index ->
{:ok, apply_into([], index, value), nil}
end
end
# leaf operation: does map have token?
defp walk_container(operation, _parent, map, "**", tokens, _value, _options)
when is_get_map(operation, map, tokens) do
{:ok, map, nil}
end
# leaf operation: does map have token?
defp walk_container(operation, _parent, map, "", tokens, _value, _options)
when is_get_map(operation, map, tokens) do
case Map.fetch(map, "") do
{:ok, existing} ->
{:ok, existing, nil}
:error ->
{:ok, map, nil}
end
end
# leaf operation: does map have token?
defp walk_container(operation, _parent, map, token, tokens, _value, _options)
when is_get_map(operation, map, tokens) do
case Map.fetch(map, token) do
{:ok, existing} ->
{:ok, existing, nil}
:error ->
{:error, "token not found: #{token}", map}
end
end
# leaf operation: does list have index?
defp walk_container(operation, _parent, list, "**", tokens, _value, _options)
when is_get_list(operation, list, tokens) do
{:ok, list, nil}
end
# leaf operation: does list have index?
defp walk_container(operation, _parent, list, token, tokens, _value, _options)
when is_get_list(operation, list, tokens) do
case parse_index(token) do
{:error, msg} ->
{:error, msg, list}
index when is_binary(index) ->
{:error, "invalid index: #{token}", list}
index ->
if index < Enum.count(list) && Enum.at(list, index) != nil do
{:ok, Enum.at(list, index), nil}
else
{:error, "index out of bounds: #{index}", list}
end
end
end
#
defp walk_container(operation, _parent, map, "**", tokens, value, options) when is_map(map) do
[next_token | next_tokens] = tokens
next_result(
Map.fetch(map, next_token),
operation,
map,
"**",
next_token,
next_tokens,
value,
options
)
end
defp walk_container(operation, _parent, list, "**", tokens, value, options)
when is_set_list_no_tokens(operation, list) do
result =
Enum.reduce(list, [], fn entry, acc ->
case walk_container(operation, list, entry, "**", tokens, value, options) do
{:ok, walk_val, _original_val} ->
acc ++ [walk_val]
{:error, _msg, _value} ->
acc
end
end)
{:ok, result, nil}
end
#
defp walk_container(operation, _parent, list, "**", tokens, value, options)
when is_list(list) do
result =
Enum.reduce(list, [], fn entry, acc ->
case walk_container(operation, list, entry, "**", tokens, value, options) do
{:ok, walk_val, _original_val} ->
acc ++ [walk_val]
{:error, _msg, _value} ->
acc
end
end)
{:ok, result, nil}
end
#
defp walk_container(_operation, _parent, map, "**", tokens, _value, _options) do
[next_token | _] = tokens
{:error, "token not found: #{next_token}", map}
end
# recursively walk through a map container
defp walk_container(operation, _parent, map, token, tokens, value, options)
when is_add_set_remove_map(operation, map) do
[next_token | next_tokens] = tokens
next_result(
Map.fetch(map, token),
operation,
map,
token,
next_token,
next_tokens,
value,
options
)
end
# recursively walk through a map container
defp walk_container(operation, _parent, map, token, tokens, value, options) when is_map(map) do
[next_token | next_tokens] = tokens
result =
case Map.fetch(map, token) do
{:ok, existing} ->
{res, sub, rem} =
walk_container(operation, map, existing, next_token, next_tokens, value, options)
# re-apply the altered tree back into our map
if res == :ok do
if next_token == "**" do
{res, sub, rem}
else
{res, sub, rem}
end
else
{res, sub, rem}
end
:error ->
case operation do
:has ->
{_res, _sub, _rem} =
walk_container(operation, map, %{}, next_token, next_tokens, value, options)
_ ->
{:error, "json pointer key not found: #{token}", map}
end
end
result
end
defp walk_container(operation, _parent, list, token, tokens, value, options)
when is_set_remove_list(operation, list) do
[next_token | tokens] = tokens
result =
case parse_index(token) do
{:error, msg} ->
{:error, msg, list}
index ->
{res, sub, rem} =
walk_container(
operation,
list,
Enum.at(list, index),
next_token,
tokens,
value,
options
)
# re-apply the returned result back into the current list - WHY!
{res, apply_into(list, index, sub), rem}
end
result
end
# recursively walk through a list container
defp walk_container(operation, _parent, list, token, tokens, value, options)
when is_list(list) do
[next_token | tokens] = tokens
result =
case parse_index(token) do
{:error, msg} ->
{:error, msg, list}
index ->
if (operation == :get or operation == :has) and index >= Enum.count(list) do
{:error, "index out of bounds: #{index}", list}
else
{res, sub, rem} =
walk_container(
operation,
list,
Enum.at(list, index),
next_token,
tokens,
value,
options
)
# a sublety of adding over setting
if operation == :add,
do: {res, apply_into(list, index, sub), list},
else: {res, sub, rem}
# {res, [sub], list}
end
end
result
end
# when there is no container defined, use the type of token to decide one
defp walk_container(operation, _parent, container, token, tokens, value, options)
when is_set_nil_container(operation, container) do
[next_token | tokens] = tokens
case parse_index(token) do
index when is_integer(index) ->
{res, sub, rem} = walk_container(operation, [], [], next_token, tokens, value, options)
# re-apply the returned result back into the current list
{res, apply_into([], index, sub), rem}
_ ->
{res, sub, rem} = walk_container(operation, %{}, %{}, next_token, tokens, value, options)
# re-apply the returned result back into the current list
{res, apply_into(%{}, token, sub), rem}
end
end
defp next_result(:error, operation, map, "**", next_token, next_tokens, value, options)
when is_set_map_no_tokens(operation, map) do
result =
Enum.reduce(map, %{}, fn {map_key, _map_value}, result ->
case walk_container(
operation,
map,
Map.fetch!(map, map_key),
"**",
[next_token] ++ next_tokens,
value,
options
) do
{:ok, rval, _res} ->
apply_into(result, map_key, rval)
{:error, msg, _value} ->
raise "error applying :set into map: #{msg}"
end
end)
{:ok, result, nil}
end
defp next_result(
{:ok, _existing},
operation,
map,
"**",
next_token,
next_tokens,
value,
options
) do
walk_container(operation, map, map, next_token, next_tokens, value, options)
end
defp next_result(:error, operation, map, "**", next_token, next_tokens, value, options)
when is_map(map) do
result =
Enum.reduce(Map.keys(map), [], fn map_key, acc ->
operation
|> walk_container(
map,
Map.fetch!(map, map_key),
"**",
[next_token] ++ next_tokens,
value,
options
)
|> next_result(acc)
end)
if List.first(result) == nil do
{:error, "token not found: #{next_token}", result}
else
{:ok, result, nil}
end
end
defp next_result(
{:ok, existing},
operation,
map,
token,
next_token,
next_tokens,
value,
options
) do
# catch the situation where the wildcard is the last token
{res, sub, rem} =
walk_container(operation, map, existing, next_token, next_tokens, value, options)
# re-apply the altered tree back into our map
if res == :ok do
{res, apply_into(map, token, sub), rem}
else
{res, sub, rem}
end
end
defp next_result(:error, operation, map, token, next_token, next_tokens, value, options) do
case Map.get(options, :strict) do
true ->
{:error, "path /#{token} does not exist", map}
_ ->
new_container = if next_token == "-", do: [], else: %{}
{res, sub, rem} =
walk_container(
operation,
map,
new_container,
next_token,
next_tokens,
value,
options
)
{res, apply_into(map, token, sub), rem}
end
end
defp next_result({:ok, walk_val, _walk_res}, acc) when is_list(walk_val), do: acc ++ walk_val
defp next_result({:ok, walk_val, _walk_res}, acc), do: acc ++ [walk_val]
defp next_result({:error, _msg, _value}, acc), do: acc
end