Current section

Files

Jump to
jsonpatch lib jsonpatch.ex
Raw

lib/jsonpatch.ex

defmodule Jsonpatch do
@moduledoc """
A implementation of [RFC 6902](https://tools.ietf.org/html/rfc6902) in pure Elixir.
The patch can be a single change or a list of things that shall be changed. Therefore
a list or a single JSON patch can be provided. Every patch belongs to a certain operation
which influences the usage.
"""
alias Jsonpatch.FlatMap
alias Jsonpatch.Operation
alias Jsonpatch.Operation.Add
alias Jsonpatch.Operation.Copy
alias Jsonpatch.Operation.Move
alias Jsonpatch.Operation.Remove
alias Jsonpatch.Operation.Replace
alias Jsonpatch.Operation.Test
@typedoc """
A valid Jsonpatch operation by RFC 6902
"""
@type t :: Add.t() | Remove.t() | Replace.t() | Copy.t() | Move.t() | Test.t()
@typedoc """
Describe an error that occured while patching.
"""
@type error :: {:error, :invalid_path | :invalid_index | :test_failed, bitstring()}
@doc """
Apply a Jsonpatch to a map or struct. The whole patch will not be applied
when any path is invalid or any other error occured.
## Examples
iex> patch = [
...> %Jsonpatch.Operation.Add{path: "/age", value: 33},
...> %Jsonpatch.Operation.Replace{path: "/hobbies/0", value: "Elixir!"},
...> %Jsonpatch.Operation.Replace{path: "/married", value: true},
...> %Jsonpatch.Operation.Remove{path: "/hobbies/1"},
...> %Jsonpatch.Operation.Remove{path: "/hobbies/2"},
...> %Jsonpatch.Operation.Copy{from: "/name", path: "/surname"},
...> %Jsonpatch.Operation.Move{from: "/home", path: "/work"},
...> %Jsonpatch.Operation.Test{path: "/name", value: "Bob"}
...> ]
iex> target = %{"name" => "Bob", "married" => false, "hobbies" => ["Sport", "Elixir", "Football"], "home" => "Berlin"}
iex> Jsonpatch.apply_patch(patch, target)
%{"name" => "Bob", "married" => true, "hobbies" => ["Elixir!"], "age" => 33, "surname" => "Bob", "work" => "Berlin"}
iex> # Patch will not be applied if test fails. The target will not be changed.
iex> patch = [
...> %Jsonpatch.Operation.Add{path: "/age", value: 33},
...> %Jsonpatch.Operation.Test{path: "/name", value: "Alice"}
...> ]
iex> target = %{"name" => "Bob", "married" => false, "hobbies" => ["Sport", "Elixir", "Football"], "home" => "Berlin"}
iex> Jsonpatch.apply_patch(patch, target)
{:error, :test_failed, "Expected value 'Alice' at '/name'"}
"""
@spec(
apply_patch(Jsonpatch.t() | list(Jsonpatch.t()), map()) ::
map(),
Jsonpatch.t() | list(Jsonpatch.t()) | Jsonpatch.error()
)
def apply_patch(json_patch, target)
def apply_patch(json_patch, %{} = target) when is_list(json_patch) do
# Operatons MUST be sorted before applying because a remove operation for path "/foo/2" must be done
# before the remove operation for path "/foo/1". Without order it could be possible that the wrong
# value will be removed or only one value instead of two.
result =
json_patch
|> Enum.map(&create_sort_value/1)
|> Enum.sort(fn {sort_value_1, _}, {sort_value_2, _} -> sort_value_1 >= sort_value_2 end)
|> Enum.map(fn {_, patch} -> patch end)
|> Enum.reduce(target, &Jsonpatch.Operation.apply_op/2)
case result do
{:error, _, _} = error -> error
ok_result -> ok_result
end
end
def apply_patch(json_patch, %{} = target) do
Operation.apply_op(json_patch, target)
end
@doc """
Creates a patch from the difference of a source map to a target map.
## Examples
iex> source = %{"name" => "Bob", "married" => false, "hobbies" => ["Elixir", "Sport", "Football"]}
iex> destination = %{"name" => "Bob", "married" => true, "hobbies" => ["Elixir!"], "age" => 33}
iex> Jsonpatch.diff(source, destination)
[
%Add{path: "/age", value: 33},
%Replace{path: "/hobbies/0", value: "Elixir!"},
%Replace{path: "/married", value: true},
%Remove{path: "/hobbies/1"},
%Remove{path: "/hobbies/2"}
]
"""
@spec diff(map, map) :: list(Jsonpatch.t())
def diff(source, destination)
def diff(%{} = source, %{} = destination) do
source = FlatMap.parse(source)
destination = FlatMap.parse(destination)
[]
|> create_additions(source, destination)
|> create_replaces(source, destination)
|> create_removes(source, destination)
end
@doc """
Creates "add"-operations by using the keys of the destination and check their existence in the
source map. Source and destination has to be parsed to a flat map.
"""
@spec create_additions(list(Jsonpatch.t()), map, map) :: list(Jsonpatch.t())
def create_additions(accumulator \\ [], source, destination)
def create_additions(accumulator, %{} = source, %{} = destination) do
additions =
Map.keys(destination)
|> Enum.filter(fn key -> not Map.has_key?(source, key) end)
|> Enum.map(fn key ->
%Add{path: key, value: Map.get(destination, key)}
end)
accumulator ++ additions
end
@doc """
Creates "remove"-operations by using the keys of the destination and check their existence in the
source map. Source and destination has to be parsed to a flat map.
"""
@spec create_removes(list(Jsonpatch.t()), map, map) :: list(Jsonpatch.t())
def create_removes(accumulator \\ [], source, destination)
def create_removes(accumulator, %{} = source, %{} = destination) do
removes =
Map.keys(source)
|> Enum.filter(fn key -> not Map.has_key?(destination, key) end)
|> Enum.map(fn key -> %Remove{path: key} end)
accumulator ++ removes
end
@doc """
Creates "replace"-operations by comparing keys and values of source and destination. The source and
destination map have to be flat maps.
"""
@spec create_replaces(list(Jsonpatch.t()), map, map) :: list(Jsonpatch.t())
def create_replaces(accumulator \\ [], source, destination)
def create_replaces(accumulator, source, destination) do
replaces =
Map.keys(destination)
|> Enum.filter(fn key -> Map.has_key?(source, key) end)
|> Enum.filter(fn key -> Map.get(source, key) != Map.get(destination, key) end)
|> Enum.map(fn key ->
%Replace{path: key, value: Map.get(destination, key)}
end)
accumulator ++ replaces
end
# ===== ===== PRIVATE ===== =====
# Create once a easy sortable value for a operation
defp create_sort_value(%{path: path} = operation) do
fragments = String.split(path, "/")
x = Jsonpatch.PathUtil.operation_sort_value?(operation) * 1_000_000 * 100_000_000
y = length(fragments) * 100_000_000
z =
case List.last(fragments) |> Integer.parse() do
:error -> 0
{int, _} -> int
end
# Structure of recorde sort value
# x = Kind of PathUtil
# y = Amount of fragments (how deep goes the path?)
# z = At which position in a list?
# xxxxyyyyyyzzzzzzzz
{x + y + z, operation}
end
end