Skip to content

Commit 767eecd

Browse files
committed
bugfix: incorrect apply_patch for the move operation for the same list
1 parent 0224a7b commit 767eecd

File tree

2 files changed

+106
-5
lines changed

2 files changed

+106
-5
lines changed

lib/jsonpatch/operation/move.ex

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ defmodule Jsonpatch.Operation.Move do
1010
{:ok, %{"a" => %{"e" => %{"c" => "Bob"}}, "d" => false}}
1111
"""
1212

13-
alias Jsonpatch.Operation.{Copy, Move, Remove}
13+
alias Jsonpatch.Operation.{Add, Move, Remove}
1414
alias Jsonpatch.Types
15+
alias Jsonpatch.Utils
1516

1617
@enforce_keys [:from, :path]
1718
defstruct [:from, :path]
@@ -28,12 +29,28 @@ defmodule Jsonpatch.Operation.Move do
2829
end
2930

3031
defp do_move(from, path, target, opts) do
31-
copy_patch = %Copy{from: from, path: path}
3232
remove_patch = %Remove{path: from}
3333

34-
with {:ok, res} <- Copy.apply(copy_patch, target, opts),
35-
{:ok, res} <- Remove.apply(remove_patch, res, opts) do
34+
with {:ok, destination} <- Utils.get_destination(target, from, opts),
35+
{:ok, from_fragments} = Utils.split_path(from),
36+
{:ok, copy_value} <- extract_copy_value(destination, from_fragments),
37+
{:ok, res} <- Remove.apply(remove_patch, target, opts),
38+
{:ok, res} <- Add.apply(%Add{value: copy_value, path: path}, res, opts) do
3639
{:ok, res}
3740
end
3841
end
42+
43+
defp extract_copy_value({%{} = destination, fragment}, from_path) do
44+
case destination do
45+
%{^fragment => val} -> {:ok, val}
46+
_ -> {:error, {:invalid_path, from_path}}
47+
end
48+
end
49+
50+
defp extract_copy_value({destination, index}, from_path) when is_list(destination) do
51+
case Utils.fetch(destination, index) do
52+
{:ok, _} = ok -> ok
53+
{:error, :invalid_path} -> {:error, {:invalid_path, from_path}}
54+
end
55+
end
3956
end

test/jsonpatch/operation/move_test.exs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,96 @@ defmodule Jsonpatch.Operation.MoveTest do
22
use ExUnit.Case
33
doctest Jsonpatch.Operation.Move
44

5-
# Move is a combination of the copy and remove operation.
5+
# Basic move operation with atoms
66
test "move a value with atoms" do
77
move = %Jsonpatch.Operation.Move{from: "/a/b", path: "/a/e"}
88
target = %{a: %{b: %{c: "Bob"}}, d: false}
99

1010
assert Jsonpatch.Operation.Move.apply(move, target, keys: :atoms) ==
1111
{:ok, %{a: %{e: %{c: "Bob"}}, d: false}}
1212
end
13+
14+
# Move within the same list
15+
test "move element within the same list" do
16+
move = %Jsonpatch.Operation.Move{from: "/arr/0", path: "/arr/2"}
17+
target = %{"arr" => ["a", "b", "c"]}
18+
19+
assert Jsonpatch.Operation.Move.apply(move, target, []) ==
20+
{:ok, %{"arr" => ["b", "c", "a"]}}
21+
end
22+
23+
# Move to end of list using "-" syntax
24+
test "move element to end of list using -" do
25+
move = %Jsonpatch.Operation.Move{from: "/arr/0", path: "/arr/-"}
26+
target = %{"arr" => ["a", "b", "c"]}
27+
28+
assert Jsonpatch.Operation.Move.apply(move, target, []) ==
29+
{:ok, %{"arr" => ["b", "c", "a"]}}
30+
end
31+
32+
# Move between different lists
33+
test "move element between different lists" do
34+
move = %Jsonpatch.Operation.Move{from: "/arr1/0", path: "/arr2/1"}
35+
target = %{"arr1" => ["a", "b"], "arr2" => ["x", "y", "z"]}
36+
37+
assert Jsonpatch.Operation.Move.apply(move, target, []) ==
38+
{:ok, %{"arr1" => ["b"], "arr2" => ["x", "a", "y", "z"]}}
39+
end
40+
41+
# Move from list to object
42+
test "move element from list to object" do
43+
move = %Jsonpatch.Operation.Move{from: "/arr/0", path: "/obj/key"}
44+
target = %{"arr" => ["value"], "obj" => %{"existing" => "data"}}
45+
46+
assert Jsonpatch.Operation.Move.apply(move, target, []) ==
47+
{:ok, %{"arr" => [], "obj" => %{"existing" => "data", "key" => "value"}}}
48+
end
49+
50+
# Move from object to list
51+
test "move element from object to list" do
52+
move = %Jsonpatch.Operation.Move{from: "/obj/key", path: "/arr/0"}
53+
target = %{"obj" => %{"key" => "value"}, "arr" => ["existing"]}
54+
55+
assert Jsonpatch.Operation.Move.apply(move, target, []) ==
56+
{:ok, %{"obj" => %{}, "arr" => ["value", "existing"]}}
57+
end
58+
59+
# Move with nested paths
60+
test "move nested list element" do
61+
move = %Jsonpatch.Operation.Move{from: "/nested/arr/0", path: "/nested/arr/2"}
62+
target = %{"nested" => %{"arr" => ["a", "b", "c"]}}
63+
64+
assert Jsonpatch.Operation.Move.apply(move, target, []) ==
65+
{:ok, %{"nested" => %{"arr" => ["b", "c", "a"]}}}
66+
end
67+
68+
# Edge case: from and path are the same
69+
test "move when from and path are the same" do
70+
move = %Jsonpatch.Operation.Move{from: "/arr/0", path: "/arr/0"}
71+
target = %{"arr" => ["a", "b", "c"]}
72+
73+
assert Jsonpatch.Operation.Move.apply(move, target, []) ==
74+
{:ok, %{"arr" => ["a", "b", "c"]}}
75+
end
76+
77+
# Error cases
78+
test "move with invalid source path" do
79+
move = %Jsonpatch.Operation.Move{from: "/arr/10", path: "/arr/0"}
80+
target = %{"arr" => ["a", "b", "c"]}
81+
82+
assert match?(
83+
{:error, {:invalid_path, ["arr", "10"]}},
84+
Jsonpatch.Operation.Move.apply(move, target, [])
85+
)
86+
end
87+
88+
test "move with invalid destination path" do
89+
move = %Jsonpatch.Operation.Move{from: "/arr/0", path: "/arr/10"}
90+
target = %{"arr" => ["a", "b", "c"]}
91+
92+
assert match?(
93+
{:error, {:invalid_path, ["arr", "10"]}},
94+
Jsonpatch.Operation.Move.apply(move, target, [])
95+
)
96+
end
1397
end

0 commit comments

Comments
 (0)