Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.0.0
- Allow lists at top level of Jsonpatch.apply_patch
- Fix error message when updating a non existing key in list

# 0.13.1
- Make Jsonpatch faster by (un)escaping conditional

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ The package can be installed by adding `jsonpatch` to your list of dependencies
```elixir
def deps do
[
{:jsonpatch, "~> 0.13.1"}
{:jsonpatch, "~> 1.0.0"}
]
end
```
Expand Down
4 changes: 2 additions & 2 deletions lib/jsonpatch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ defmodule Jsonpatch do
{:ok, map()} | Jsonpatch.error()
def apply_patch(json_patch, target, opts \\ [])

def apply_patch(json_patch, %{} = target, opts) when is_list(json_patch) do
def apply_patch(json_patch, target, opts) when is_list(json_patch) do
# https://datatracker.ietf.org/doc/html/rfc6902#section-3
# > Operations are applied sequentially in the order they appear in the array.
result =
Expand All @@ -86,7 +86,7 @@ defmodule Jsonpatch do
end
end

def apply_patch(json_patch, %{} = target, opts) do
def apply_patch(json_patch, target, opts) do
apply_patch([json_patch], target, opts)
end

Expand Down
3 changes: 2 additions & 1 deletion lib/jsonpatch/operation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defprotocol Jsonpatch.Operation do
@doc """
Executes the given patch to map/struct. Possible options are defined in `Jsonpatch`.
"""
@spec apply_op(Jsonpatch.t(), map() | Jsonpatch.error(), keyword()) :: map() | Jsonpatch.error()
@spec apply_op(Jsonpatch.t(), list() | map() | Jsonpatch.error(), keyword()) ::
map() | Jsonpatch.error()
def apply_op(patch, target, opts \\ [])
end
6 changes: 3 additions & 3 deletions lib/jsonpatch/operation/add.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ defmodule Jsonpatch.Operation.Add do
@type t :: %__MODULE__{path: String.t(), value: any}

defimpl Operation do
@spec apply_op(Add.t(), map | Jsonpatch.error(), keyword()) :: map
@spec apply_op(Add.t(), list() | map() | Jsonpatch.error(), keyword()) :: map
def apply_op(_, {:error, _, _} = error, _opt), do: error

def apply_op(%Add{path: path, value: value}, %{} = target, opts) do
def apply_op(%Add{path: path, value: value}, target, opts) do
PathUtil.get_final_destination(target, path, opts)
|> do_add(target, path, value, opts)
end
Expand Down Expand Up @@ -56,7 +56,7 @@ defmodule Jsonpatch.Operation.Add do
if last_fragment == "-" or length(final_destination) == index do
Enum.concat(final_destination, [value])
else
List.update_at(final_destination, index, fn _ -> value end)
List.replace_at(final_destination, index, value)
end

PathUtil.update_final_destination(
Expand Down
4 changes: 2 additions & 2 deletions lib/jsonpatch/operation/copy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ defmodule Jsonpatch.Operation.Copy do
@type t :: %__MODULE__{from: String.t(), path: String.t()}

defimpl Operation do
@spec apply_op(Copy.t(), map() | Jsonpatch.error(), keyword()) :: map()
@spec apply_op(Copy.t(), list() | map() | Jsonpatch.error(), keyword()) :: map()
def apply_op(_, {:error, _, _} = error, _opts), do: error

def apply_op(%Copy{from: from, path: path}, target, opts) do
Expand Down Expand Up @@ -98,7 +98,7 @@ defmodule Jsonpatch.Operation.Copy do

{index, _} ->
if index < length(copy_target) do
List.update_at(copy_target, index, fn _old -> copied_value end)
List.replace_at(copy_target, index, copied_value)
else
{:error, :invalid_index, copy_path_end}
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jsonpatch/operation/move.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule Jsonpatch.Operation.Move do
@type t :: %__MODULE__{from: String.t(), path: String.t()}

defimpl Operation do
@spec apply_op(Move.t(), map | Jsonpatch.error(), keyword()) ::
@spec apply_op(Move.t(), list() | map() | Jsonpatch.error(), keyword()) ::
map()
def apply_op(_, {:error, _, _} = error, _opts), do: error

Expand Down
9 changes: 2 additions & 7 deletions lib/jsonpatch/operation/remove.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ defmodule Jsonpatch.Operation.Remove do
@type t :: %__MODULE__{path: String.t()}

defimpl Operation do
@spec apply_op(Remove.t(), map | Jsonpatch.error(), keyword()) ::
@spec apply_op(Remove.t(), list() | map() | Jsonpatch.error(), keyword()) ::
map()
def apply_op(_, {:error, _, _} = error, _opts), do: error

Expand Down Expand Up @@ -77,12 +77,7 @@ defmodule Jsonpatch.Operation.Remove do
{:error, :invalid_index, fragment}

{index, _} ->
update_list = List.update_at(target, index, &do_remove(&1, tail))

case List.pop_at(target, index) do
{nil, _} -> {:error, :invalid_index, fragment}
_ -> update_list
end
PathUtil.update_at(target, index, tail, &do_remove/2)
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/jsonpatch/operation/replace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ defmodule Jsonpatch.Operation.Replace do
@type t :: %__MODULE__{path: String.t(), value: any}

defimpl Operation do
@spec apply_op(Replace.t(), map | Jsonpatch.error(), keyword()) :: map
@spec apply_op(Replace.t(), list() | map() | Jsonpatch.error(), keyword()) :: map
def apply_op(_, {:error, _, _} = error, _opts), do: error

def apply_op(%Replace{path: path, value: value}, %{} = target, opts) do
def apply_op(%Replace{path: path, value: value}, target, opts) do
{final_destination, last_fragment} = PathUtil.get_final_destination(target, path, opts)

case do_update(final_destination, last_fragment, value) do
Expand Down
4 changes: 2 additions & 2 deletions lib/jsonpatch/operation/test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ defmodule Jsonpatch.Operation.Test do
@type t :: %__MODULE__{path: String.t(), value: any}

defimpl Operation do
@spec apply_op(Test.t(), map | Jsonpatch.error(), keyword()) :: map()
@spec apply_op(Test.t(), list() | map() | Jsonpatch.error(), keyword()) :: map()
def apply_op(_, {:error, _, _} = error, _opts), do: error

def apply_op(%Test{path: path, value: value}, %{} = target, opts) do
def apply_op(%Test{path: path, value: value}, target, opts) do
case PathUtil.get_final_destination(target, path, opts) |> do_test(value) do
true -> target
false -> {:error, :test_failed, "Expected value '#{value}' at '#{path}'"}
Expand Down
34 changes: 31 additions & 3 deletions lib/jsonpatch/path_util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,23 @@ defmodule Jsonpatch.PathUtil do
Enum.map(fragements, converter)
end

@doc """
Updates a list with the given update_fn while respecting Jsonpatch errors.
In case uodate_fn returns an error then update_at will also return this error.
When the update_fn succeeds it will return the list.
"""
@spec update_at(list(), integer(), list(binary()), (any, list(binary()) -> any)) ::
list | {:error, any, any}
def update_at(target, index, subpath, update_fn) do
case get_at(target, index) do
{:ok, old_val} ->
do_update_at(target, index, old_val, subpath, update_fn)

{:error, _, _} = error ->
error
end
end

# ===== ===== PRIVATE ===== =====

defp find_final_destination(%{} = target, [fragment | []]) do
Expand Down Expand Up @@ -170,7 +187,8 @@ defmodule Jsonpatch.PathUtil do
when is_list(target) do
{index, _} = Integer.parse(fragment)

List.update_at(target, index, &do_update_final_destination(&1, new_final_dest, tail))
update_fn = &do_update_final_destination(&1, new_final_dest, &2)
update_at(target, index, tail, update_fn)
end

defp to_atom(fragment) do
Expand Down Expand Up @@ -202,7 +220,17 @@ defmodule Jsonpatch.PathUtil do
end
end

defp do_unescape(fragment, _pattern, _replacement) do
fragment
def get_at(list, index) do
case Enum.at(list, index) do
nil -> {:error, :invalid_index, index}
ele -> {:ok, ele}
end
end

def do_update_at(list, index, target, subpath, update_fn) do
case update_fn.(target, subpath) do
{:error, _, _} = error -> error
new_val -> List.replace_at(list, index, new_val)
end
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Jsonpatch.MixProject do
app: :jsonpatch,
name: "Jsonpatch",
description: "Implementation of RFC 6902 in pure Elixir",
version: "0.13.1",
version: "1.0.0",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps(),
Expand Down
26 changes: 25 additions & 1 deletion test/jsonpatch/operation/remove_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ defmodule RemoveTest do

# Longer path, numeric - out of
remove_patch = %Remove{path: "/hobbies/1/description"}
assert {:error, :invalid_index, "1"} = Operation.apply_op(remove_patch, target)
assert {:error, :invalid_index, 1} = Operation.apply_op(remove_patch, target)
end

test "Return error when patch error was provided to remove operation" do
Expand All @@ -98,4 +98,28 @@ defmodule RemoveTest do

assert ^error = Operation.apply_op(patch, error)
end

test "Remove in list" do
# Arrange
source = [1, 2, %{"three" => 3}, 5, 6]
patch = %Remove{path: "/2/three"}

# Act
patched_source = Operation.apply_op(patch, source)

# Assert
assert [1, 2, %{}, 5, 6] = patched_source
end

test "Remove in list with wrong key" do
# Arrange
source = [1, 2, %{"three" => 3}, 5, 6]
patch = %Remove{path: "/2/four"}

# Act
patched_source = Operation.apply_op(patch, source)

# Assert
assert {:error, :invalid_path, "four"} = patched_source
end
end
14 changes: 14 additions & 0 deletions test/jsonpatch/operation/test_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,18 @@ defmodule Jsonpatch.Operation.TestTest do

assert {:error, :invalid_index, "b"} = Operation.apply_op(test, target)
end

test "Test list at top level" do
test = %Test{path: "/1", value: "bar"}
target = ["foo", "bar", "ha"]

assert ^target = Operation.apply_op(test, target)
end

test "Test list at top level with error" do
test = %Test{path: "/2", value: 3}
target = [0, 1, 2]

assert {:error, :test_failed, "Expected value '3' at '/2'"} = Operation.apply_op(test, target)
end
end
14 changes: 14 additions & 0 deletions test/jsonpatch_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ defmodule JsonpatchTest do

doctest Jsonpatch

test "Create diff from list and apply it" do
# Arrange
source = [1, 2, %{"drei" => 3}, 5, 6]
destination = [1, 2, %{"three" => 3}, 4, 5]

# Act
patch = Jsonpatch.diff(source, destination)

patched_source = Jsonpatch.apply_patch!(patch, source)

# Assert
assert ^destination = patched_source
end

# ===== DIFF =====
describe "Create diffs" do
test "adding an Object Member" do
Expand Down