Skip to content

Commit 2e1ed5b

Browse files
authored
Merge pull request #33 from Valian/set-initial-path-option
Add ancestor_path option to diff function for nested structures
2 parents 46fc20d + 3554932 commit 2e1ed5b

File tree

3 files changed

+73
-12
lines changed

3 files changed

+73
-12
lines changed

lib/jsonpatch.ex

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ defmodule Jsonpatch do
163163
@doc """
164164
Creates a patch from the difference of a source map to a destination map or list.
165165
166+
## Options
167+
168+
* `:ancestor_path` - Sets the initial ancestor path for the diff operation.
169+
Defaults to `""` (root). Useful when you need to diff starting from a nested path.
170+
166171
## Examples
167172
168173
iex> source = %{"name" => "Bob", "married" => false, "hobbies" => ["Elixir", "Sport", "Football"]}
@@ -175,20 +180,30 @@ defmodule Jsonpatch do
175180
%{path: "/hobbies/0", value: "Elixir!", op: "replace"},
176181
%{path: "/age", value: 33, op: "add"}
177182
]
183+
184+
iex> source = %{"a" => 1, "b" => 2}
185+
iex> destination = %{"a" => 3, "c" => 4}
186+
iex> Jsonpatch.diff(source, destination, ancestor_path: "/nested")
187+
[
188+
%{path: "/nested/b", op: "remove"},
189+
%{path: "/nested/c", value: 4, op: "add"},
190+
%{path: "/nested/a", value: 3, op: "replace"}
191+
]
178192
"""
179-
@spec diff(Types.json_container(), Types.json_container()) :: [Jsonpatch.t()]
180-
def diff(source, destination)
193+
@spec diff(Types.json_container(), Types.json_container(), Types.opts_diff()) :: [Jsonpatch.t()]
194+
def diff(source, destination, opts \\ []) do
195+
opts = Keyword.validate!(opts, ancestor_path: "")
181196

182-
def diff(%{} = source, %{} = destination) do
183-
do_map_diff(destination, source)
184-
end
197+
cond do
198+
is_map(source) and is_map(destination) ->
199+
do_map_diff(destination, source, opts[:ancestor_path])
185200

186-
def diff(source, destination) when is_list(source) and is_list(destination) do
187-
do_list_diff(destination, source)
188-
end
201+
is_list(source) and is_list(destination) ->
202+
do_list_diff(destination, source, opts[:ancestor_path])
189203

190-
def diff(_, _) do
191-
[]
204+
true ->
205+
[]
206+
end
192207
end
193208

194209
defguardp are_unequal_maps(val1, val2) when val1 != val2 and is_map(val2) and is_map(val1)
@@ -214,7 +229,7 @@ defmodule Jsonpatch do
214229
patches
215230
end
216231

217-
defp do_map_diff(%{} = destination, %{} = source, ancestor_path \\ "", patches \\ []) do
232+
defp do_map_diff(%{} = destination, %{} = source, ancestor_path, patches \\ []) do
218233
# entrypoint for map diff, let's convert the map to a list of {k, v} tuples
219234
destination
220235
|> Map.to_list()
@@ -245,7 +260,7 @@ defmodule Jsonpatch do
245260
do_map_diff(rest, source, ancestor_path, patches, [key | checked_keys])
246261
end
247262

248-
defp do_list_diff(destination, source, ancestor_path \\ "", patches \\ [], idx \\ 0)
263+
defp do_list_diff(destination, source, ancestor_path, patches \\ [], idx \\ 0)
249264

250265
defp do_list_diff([], [], _path, patches, _idx), do: patches
251266

lib/jsonpatch/types.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ defmodule Jsonpatch.Types do
3232
- `:keys` - controls how path fragments are decoded.
3333
"""
3434
@type opts :: [{:keys, opt_keys()}]
35+
@type opts_diff :: [{:ancestor_path, String.t()}]
3536

3637
@type casted_array_index :: :- | non_neg_integer()
3738
@type casted_object_key :: atom() | String.t()

test/jsonpatch_test.exs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,51 @@ defmodule JsonpatchTest do
140140
assert Jsonpatch.apply_patch(patches, source, keys: :atoms) == {:ok, destination}
141141
end
142142

143+
test "Create diff with ancestor_path option for nested maps" do
144+
source = %{"a" => 1}
145+
destination = %{"a" => 3}
146+
147+
patches = Jsonpatch.diff(source, destination, ancestor_path: "/nested/object")
148+
149+
assert patches == [
150+
%{op: "replace", path: "/nested/object/a", value: 3}
151+
]
152+
end
153+
154+
test "Create diff with ancestor_path option for nested lists" do
155+
source = [1, 2, 3]
156+
destination = [1, 2, 4]
157+
158+
patches = Jsonpatch.diff(source, destination, ancestor_path: "/items")
159+
160+
assert patches == [
161+
%{op: "replace", path: "/items/2", value: 4}
162+
]
163+
end
164+
165+
test "Create diff with empty ancestor_path (default behavior)" do
166+
source = %{"a" => 1, "b" => 2}
167+
destination = %{"a" => 3, "c" => 4}
168+
169+
patches_with_option = Jsonpatch.diff(source, destination, ancestor_path: "")
170+
patches_without_option = Jsonpatch.diff(source, destination)
171+
172+
assert patches_with_option == patches_without_option
173+
end
174+
175+
test "Create diff with ancestor_path containing escaped characters" do
176+
source = %{"a" => 1}
177+
destination = %{"a" => 2}
178+
179+
patches = Jsonpatch.diff(source, destination, ancestor_path: "/escape~1me~0now")
180+
181+
expected_patches = [
182+
%{op: "replace", path: "/escape~1me~0now/a", value: 2}
183+
]
184+
185+
assert patches == expected_patches
186+
end
187+
143188
defp assert_diff_apply(source, destination) do
144189
patches = Jsonpatch.diff(source, destination)
145190
assert Jsonpatch.apply_patch(patches, source) == {:ok, destination}

0 commit comments

Comments
 (0)