Skip to content

Commit 3977552

Browse files
authored
Merge pull request #35 from Valian/bugfix-changing-map-type
Bugfix: prepare_map returning non-map works correctly + small refactor.
2 parents ac77903 + 2584690 commit 3977552

2 files changed

Lines changed: 45 additions & 34 deletions

File tree

lib/jsonpatch.ex

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -208,44 +208,38 @@ defmodule Jsonpatch do
208208
|> Keyword.update(:object_hash, nil, &make_safe_hash_fn/1)
209209
|> Keyword.validate!(
210210
ancestor_path: "",
211-
prepare_map: fn struct -> struct end,
211+
prepare_map: &Function.identity/1,
212212
object_hash: nil
213213
)
214214

215-
cond do
216-
is_map(source) and is_map(destination) ->
217-
do_map_diff(destination, source, opts[:ancestor_path], [], opts)
218-
219-
is_list(source) and is_list(destination) ->
220-
do_list_diff(destination, source, opts[:ancestor_path], [], 0, opts)
221-
222-
# type of value changed, eg set to nil
223-
source != destination ->
224-
destination = maybe_prepare_map(destination, opts)
225-
[%{op: "replace", path: opts[:ancestor_path], value: destination}]
226-
227-
true ->
228-
[]
229-
end
215+
do_diff(destination, source, opts[:ancestor_path], nil, [], opts)
230216
end
231217

232218
defguardp are_unequal_maps(val1, val2) when val1 != val2 and is_map(val2) and is_map(val1)
233219
defguardp are_unequal_lists(val1, val2) when val1 != val2 and is_list(val2) and is_list(val1)
234220

235221
defp do_diff(dest, source, path, key, patches, opts) when are_unequal_lists(dest, source) do
236222
# uneqal lists, let's use a specialized function for that
237-
do_list_diff(dest, source, "#{path}/#{escape(key)}", patches, 0, opts)
223+
do_list_diff(dest, source, join_key(path, key), patches, opts)
238224
end
239225

240226
defp do_diff(dest, source, path, key, patches, opts) when are_unequal_maps(dest, source) do
241-
# uneqal maps, let's use a specialized function for that
242-
do_map_diff(dest, source, "#{path}/#{escape(key)}", patches, opts)
227+
# Convert structs to maps if prepare_map function is provided
228+
dest = maybe_prepare_map(dest, opts)
229+
source = maybe_prepare_map(source, opts)
230+
231+
if not is_map(dest) or not is_map(source) do
232+
# type changed, let's process it again
233+
do_diff(dest, source, path, key, patches, opts)
234+
else
235+
# uneqal maps, let's use a specialized function for that
236+
do_map_diff(dest, source, join_key(path, key), patches, opts)
237+
end
243238
end
244239

245240
defp do_diff(dest, source, path, key, patches, opts) when dest != source do
246241
# scalar values or change of type (map -> list etc), let's just make a replace patch
247-
value = maybe_prepare_map(dest, opts)
248-
[%{op: "replace", path: "#{path}/#{escape(key)}", value: value} | patches]
242+
[%{op: "replace", path: join_key(path, key), value: maybe_prepare_map(dest, opts)} | patches]
249243
end
250244

251245
defp do_diff(_dest, _source, _path, _key, patches, _opts) do
@@ -254,10 +248,6 @@ defmodule Jsonpatch do
254248
end
255249

256250
defp do_map_diff(%{} = destination, %{} = source, ancestor_path, patches, opts) do
257-
# Convert structs to maps if prepare_map function is provided
258-
destination = maybe_prepare_map(destination, opts)
259-
source = maybe_prepare_map(source, opts)
260-
261251
# entrypoint for map diff, let's convert the map to a list of {k, v} tuples
262252
destination
263253
|> Map.to_list()
@@ -271,7 +261,7 @@ defmodule Jsonpatch do
271261
if k in checked_keys do
272262
patches
273263
else
274-
[%{op: "remove", path: "#{ancestor_path}/#{escape(k)}"} | patches]
264+
[%{op: "remove", path: join_key(ancestor_path, k)} | patches]
275265
end
276266
end)
277267
end
@@ -285,23 +275,23 @@ defmodule Jsonpatch do
285275

286276
:error ->
287277
value = maybe_prepare_map(val, opts)
288-
[%{op: "add", path: "#{ancestor_path}/#{escape(key)}", value: value} | patches]
278+
[%{op: "add", path: join_key(ancestor_path, key), value: value} | patches]
289279
end
290280

291281
# Diff next value of same level
292282
do_map_diff(rest, source, ancestor_path, patches, [key | checked_keys], opts)
293283
end
294284

295-
defp do_list_diff(destination, source, ancestor_path, patches, idx, opts) do
285+
defp do_list_diff(destination, source, ancestor_path, patches, opts) do
296286
if opts[:object_hash] do
297287
do_hash_list_diff(destination, source, ancestor_path, patches, opts)
298288
else
299-
do_pairwise_list_diff(destination, source, ancestor_path, patches, idx, opts)
289+
do_pairwise_list_diff(destination, source, ancestor_path, patches, 0, opts)
300290
end
301291
catch
302292
# happens if we've got a nil hash or we tried to hash a non-map
303293
:hash_not_implemented ->
304-
do_pairwise_list_diff(destination, source, ancestor_path, patches, idx, opts)
294+
do_pairwise_list_diff(destination, source, ancestor_path, patches, 0, opts)
305295
end
306296

307297
defp do_pairwise_list_diff(destination, source, ancestor_path, patches, idx, opts)
@@ -310,15 +300,15 @@ defmodule Jsonpatch do
310300

311301
defp do_pairwise_list_diff([], [_item | source_rest], ancestor_path, patches, idx, opts) do
312302
# if we find any leftover items in source, we have to remove them
313-
patches = [%{op: "remove", path: "#{ancestor_path}/#{idx}"} | patches]
303+
patches = [%{op: "remove", path: join_key(ancestor_path, idx)} | patches]
314304
do_pairwise_list_diff([], source_rest, ancestor_path, patches, idx + 1, opts)
315305
end
316306

317307
defp do_pairwise_list_diff(items, [], ancestor_path, patches, idx, opts) do
318308
# we have to do it without recursion, because we have to keep the order of the items
319309
items
320310
|> Enum.map_reduce(idx, fn val, idx ->
321-
{%{op: "add", path: "#{ancestor_path}/#{idx}", value: maybe_prepare_map(val, opts)},
311+
{%{op: "add", path: join_key(ancestor_path, idx), value: maybe_prepare_map(val, opts)},
322312
idx + 1}
323313
end)
324314
|> elem(0)
@@ -469,15 +459,15 @@ defmodule Jsonpatch do
469459
@compile {:inline, add_removals: 4}
470460
defp add_removals(from_idx, to_idx, path, removals) do
471461
Enum.reduce(from_idx..to_idx//1, removals, fn idx, removals ->
472-
[%{op: "remove", path: "#{path}/#{idx}"} | removals]
462+
[%{op: "remove", path: join_key(path, idx)} | removals]
473463
end)
474464
end
475465

476466
@compile {:inline, add_additions: 6}
477467
defp add_additions(from_idx, to_idx, path, dest_tuple, additions, opts) do
478468
Enum.reduce(from_idx..to_idx//1, additions, fn idx, additions ->
479469
value = dest_tuple |> elem(idx) |> maybe_prepare_map(opts)
480-
[%{op: "add", path: "#{path}/#{idx}", value: value} | additions]
470+
[%{op: "add", path: join_key(path, idx), value: value} | additions]
481471
end)
482472
end
483473

@@ -503,6 +493,10 @@ defmodule Jsonpatch do
503493

504494
defp escape(fragment), do: fragment
505495

496+
@compile {:inline, join_key: 2}
497+
defp join_key(path, nil), do: path
498+
defp join_key(path, key), do: "#{path}/#{escape(key)}"
499+
506500
defp make_safe_hash_fn(hash_fn) do
507501
# we want to compare only maps, and returning nil should mean
508502
# we should compare lists pairwise instead

test/jsonpatch_test.exs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,23 @@ defmodule JsonpatchTest do
324324
]
325325
end
326326

327+
test "replace_map changing type of value is also supported" do
328+
source = %{"a" => ~D[2025-01-01]}
329+
destination = %{"a" => ~D[2025-01-02]}
330+
331+
patches =
332+
Jsonpatch.diff(source, destination,
333+
prepare_map: fn
334+
%Date{year: year, month: month, day: day} -> "#{year}-#{month}-#{day}"
335+
map -> map
336+
end
337+
)
338+
339+
assert patches == [
340+
%{op: "replace", path: "/a", value: "2025-1-2"}
341+
]
342+
end
343+
327344
test "Create diff with ancestor_path when changing type of base value (map to nil)" do
328345
source = %{"key" => "value"}
329346
destination = nil

0 commit comments

Comments
 (0)