@@ -167,6 +167,17 @@ defmodule Jsonpatch do
167167
168168 * `:ancestor_path` - Sets the initial ancestor path for the diff operation.
169169 Defaults to `""` (root). Useful when you need to diff starting from a nested path.
170+ * `:prepare_map` - A function that lets to customize maps and structs before diffing.
171+ Defaults to `fn map -> map end` (no-op). Useful when you need to customize
172+ how maps and structs are handled during the diff process. Example:
173+
174+ ```elixir
175+ fn
176+ %Struct{field1: value1, field2: value2} -> %{field1: "\# {value1} - \# {value2}"}
177+ %OtherStruct{} = struct -> Map.take(struct, [:field1])
178+ map -> map
179+ end
180+ ```
170181
171182 ## Examples
172183
@@ -192,14 +203,24 @@ defmodule Jsonpatch do
192203 """
193204 @ spec diff ( Types . json_container ( ) , Types . json_container ( ) , Types . opts_diff ( ) ) :: [ Jsonpatch . t ( ) ]
194205 def diff ( source , destination , opts \\ [ ] ) do
195- opts = Keyword . validate! ( opts , ancestor_path: "" )
206+ opts =
207+ Keyword . validate! ( opts ,
208+ ancestor_path: "" ,
209+ # by default, a no-op
210+ prepare_map: fn map -> map end
211+ )
196212
197213 cond do
198214 is_map ( source ) and is_map ( destination ) ->
199- do_map_diff ( destination , source , opts [ :ancestor_path ] )
215+ do_map_diff ( destination , source , opts [ :ancestor_path ] , [ ] , opts )
200216
201217 is_list ( source ) and is_list ( destination ) ->
202- do_list_diff ( destination , source , opts [ :ancestor_path ] )
218+ do_list_diff ( destination , source , opts [ :ancestor_path ] , [ ] , 0 , opts )
219+
220+ # type of value changed, eg set to nil
221+ source != destination ->
222+ destination = maybe_prepare_map ( destination , opts )
223+ [ % { op: "replace" , path: opts [ :ancestor_path ] , value: destination } ]
203224
204225 true ->
205226 [ ]
@@ -209,34 +230,39 @@ defmodule Jsonpatch do
209230 defguardp are_unequal_maps ( val1 , val2 ) when val1 != val2 and is_map ( val2 ) and is_map ( val1 )
210231 defguardp are_unequal_lists ( val1 , val2 ) when val1 != val2 and is_list ( val2 ) and is_list ( val1 )
211232
212- defp do_diff ( dest , source , path , key , patches ) when are_unequal_lists ( dest , source ) do
233+ defp do_diff ( dest , source , path , key , patches , opts ) when are_unequal_lists ( dest , source ) do
213234 # uneqal lists, let's use a specialized function for that
214- do_list_diff ( dest , source , "#{ path } /#{ escape ( key ) } " , patches )
235+ do_list_diff ( dest , source , "#{ path } /#{ escape ( key ) } " , patches , 0 , opts )
215236 end
216237
217- defp do_diff ( dest , source , path , key , patches ) when are_unequal_maps ( dest , source ) do
238+ defp do_diff ( dest , source , path , key , patches , opts ) when are_unequal_maps ( dest , source ) do
218239 # uneqal maps, let's use a specialized function for that
219- do_map_diff ( dest , source , "#{ path } /#{ escape ( key ) } " , patches )
240+ do_map_diff ( dest , source , "#{ path } /#{ escape ( key ) } " , patches , opts )
220241 end
221242
222- defp do_diff ( dest , source , path , key , patches ) when dest != source do
243+ defp do_diff ( dest , source , path , key , patches , opts ) when dest != source do
223244 # scalar values or change of type (map -> list etc), let's just make a replace patch
224- [ % { op: "replace" , path: "#{ path } /#{ escape ( key ) } " , value: dest } | patches ]
245+ value = maybe_prepare_map ( dest , opts )
246+ [ % { op: "replace" , path: "#{ path } /#{ escape ( key ) } " , value: value } | patches ]
225247 end
226248
227- defp do_diff ( _dest , _source , _path , _key , patches ) do
249+ defp do_diff ( _dest , _source , _path , _key , patches , _opts ) do
228250 # no changes, return patches as is
229251 patches
230252 end
231253
232- defp do_map_diff ( % { } = destination , % { } = source , ancestor_path , patches \\ [ ] ) do
254+ defp do_map_diff ( % { } = destination , % { } = source , ancestor_path , patches , opts ) do
255+ # Convert structs to maps if prepare_map function is provided
256+ destination = maybe_prepare_map ( destination , opts )
257+ source = maybe_prepare_map ( source , opts )
258+
233259 # entrypoint for map diff, let's convert the map to a list of {k, v} tuples
234260 destination
235261 |> Map . to_list ( )
236- |> do_map_diff ( source , ancestor_path , patches , [ ] )
262+ |> do_map_diff ( source , ancestor_path , patches , [ ] , opts )
237263 end
238264
239- defp do_map_diff ( [ ] , source , ancestor_path , patches , checked_keys ) do
265+ defp do_map_diff ( [ ] , source , ancestor_path , patches , checked_keys , _opts ) do
240266 # The complete desination was check. Every key that is not in the list of
241267 # checked keys, must be removed.
242268 Enum . reduce ( source , patches , fn { k , _ } , patches ->
@@ -248,44 +274,56 @@ defmodule Jsonpatch do
248274 end )
249275 end
250276
251- defp do_map_diff ( [ { key , val } | rest ] , source , ancestor_path , patches , checked_keys ) do
277+ defp do_map_diff ( [ { key , val } | rest ] , source , ancestor_path , patches , checked_keys , opts ) do
252278 # normal iteration through list of map {k, v} tuples. We track seen keys to later remove not seen keys.
253279 patches =
254280 case Map . fetch ( source , key ) do
255- { :ok , source_val } -> do_diff ( val , source_val , ancestor_path , key , patches )
256- :error -> [ % { op: "add" , path: "#{ ancestor_path } /#{ escape ( key ) } " , value: val } | patches ]
281+ { :ok , source_val } ->
282+ do_diff ( val , source_val , ancestor_path , key , patches , opts )
283+
284+ :error ->
285+ value = maybe_prepare_map ( val , opts )
286+ [ % { op: "add" , path: "#{ ancestor_path } /#{ escape ( key ) } " , value: value } | patches ]
257287 end
258288
259289 # Diff next value of same level
260- do_map_diff ( rest , source , ancestor_path , patches , [ key | checked_keys ] )
290+ do_map_diff ( rest , source , ancestor_path , patches , [ key | checked_keys ] , opts )
261291 end
262292
263- defp do_list_diff ( destination , source , ancestor_path , patches \\ [ ] , idx \\ 0 )
293+ defp do_list_diff ( destination , source , ancestor_path , patches , idx , opts )
264294
265- defp do_list_diff ( [ ] , [ ] , _path , patches , _idx ) , do: patches
295+ defp do_list_diff ( [ ] , [ ] , _path , patches , _idx , _opts ) , do: patches
266296
267- defp do_list_diff ( [ ] , [ _item | source_rest ] , ancestor_path , patches , idx ) do
297+ defp do_list_diff ( [ ] , [ _item | source_rest ] , ancestor_path , patches , idx , opts ) do
268298 # if we find any leftover items in source, we have to remove them
269299 patches = [ % { op: "remove" , path: "#{ ancestor_path } /#{ idx } " } | patches ]
270- do_list_diff ( [ ] , source_rest , ancestor_path , patches , idx + 1 )
300+ do_list_diff ( [ ] , source_rest , ancestor_path , patches , idx + 1 , opts )
271301 end
272302
273- defp do_list_diff ( items , [ ] , ancestor_path , patches , idx ) do
303+ defp do_list_diff ( items , [ ] , ancestor_path , patches , idx , opts ) do
274304 # we have to do it without recursion, because we have to keep the order of the items
275305 items
276306 |> Enum . map_reduce ( idx , fn val , idx ->
277- { % { op: "add" , path: "#{ ancestor_path } /#{ idx } " , value: val } , idx + 1 }
307+ { % { op: "add" , path: "#{ ancestor_path } /#{ idx } " , value: maybe_prepare_map ( val , opts ) } ,
308+ idx + 1 }
278309 end )
279310 |> elem ( 0 )
280311 |> Kernel . ++ ( patches )
281312 end
282313
283- defp do_list_diff ( [ val | rest ] , [ source_val | source_rest ] , ancestor_path , patches , idx ) do
314+ defp do_list_diff ( [ val | rest ] , [ source_val | source_rest ] , ancestor_path , patches , idx , opts ) do
284315 # case when there's an item in both desitation and source. Let's just compare them
285- patches = do_diff ( val , source_val , ancestor_path , idx , patches )
286- do_list_diff ( rest , source_rest , ancestor_path , patches , idx + 1 )
316+ patches = do_diff ( val , source_val , ancestor_path , idx , patches , opts )
317+ do_list_diff ( rest , source_rest , ancestor_path , patches , idx + 1 , opts )
287318 end
288319
320+ defp maybe_prepare_map ( value , opts ) when is_map ( value ) do
321+ prepare_fn = Keyword . fetch! ( opts , :prepare_map )
322+ prepare_fn . ( value )
323+ end
324+
325+ defp maybe_prepare_map ( value , _opts ) , do: value
326+
289327 @ compile { :inline , escape: 1 }
290328
291329 defp escape ( fragment ) when is_binary ( fragment ) do
0 commit comments