@@ -12,7 +12,6 @@ defmodule Jsonpatch do
1212
1313 alias Jsonpatch.Types
1414 alias Jsonpatch.Operation . { Add , Copy , Move , Remove , Replace , Test }
15- alias Jsonpatch.Utils
1615
1716 @ typedoc """
1817 A valid Jsonpatch operation by RFC 6902
@@ -181,75 +180,109 @@ defmodule Jsonpatch do
181180 def diff ( source , destination )
182181
183182 def diff ( % { } = source , % { } = destination ) do
184- flat ( destination )
185- |> do_diff ( source , "" )
183+ do_map_diff ( destination , source )
186184 end
187185
188186 def diff ( source , destination ) when is_list ( source ) and is_list ( destination ) do
189- flat ( destination )
190- |> do_diff ( source , "" )
187+ do_list_diff ( destination , source )
191188 end
192189
193190 def diff ( _ , _ ) do
194191 [ ]
195192 end
196193
197- defguardp are_unequal_maps ( val1 , val2 )
198- when val1 != val2 and is_map ( val2 ) and is_map ( val1 )
194+ defguardp are_unequal_maps ( val1 , val2 ) when val1 != val2 and is_map ( val2 ) and is_map ( val1 )
195+ defguardp are_unequal_lists ( val1 , val2 ) when val1 != val2 and is_list ( val2 ) and is_list ( val1 )
199196
200- defguardp are_unequal_lists ( val1 , val2 )
201- when val1 != val2 and is_list ( val2 ) and is_list ( val1 )
197+ defp do_diff ( dest , source , path , key , patches ) when are_unequal_lists ( dest , source ) do
198+ # uneqal lists, let's use a specialized function for that
199+ do_list_diff ( dest , source , "#{ path } /#{ escape ( key ) } " , patches )
200+ end
201+
202+ defp do_diff ( dest , source , path , key , patches ) when are_unequal_maps ( dest , source ) do
203+ # uneqal maps, let's use a specialized function for that
204+ do_map_diff ( dest , source , "#{ path } /#{ escape ( key ) } " , patches )
205+ end
202206
203- # Diff reduce loop
204- defp do_diff ( destination , source , ancestor_path , acc \\ [ ] , checked_keys \\ [ ] )
207+ defp do_diff ( dest , source , path , key , patches ) when dest != source do
208+ # scalar values or change of type (map -> list etc), let's just make a replace patch
209+ [ % { op: "replace" , path: "#{ path } /#{ escape ( key ) } " , value: dest } | patches ]
210+ end
211+
212+ defp do_diff ( _dest , _source , _path , _key , patches ) do
213+ # no changes, return patches as is
214+ patches
215+ end
205216
206- defp do_diff ( [ ] , source , ancestor_path , patches , checked_keys ) do
217+ defp do_map_diff ( % { } = destination , % { } = source , ancestor_path \\ "" , patches \\ [ ] ) do
218+ # entrypoint for map diff, let's convert the map to a list of {k, v} tuples
219+ destination
220+ |> Map . to_list ( )
221+ |> do_map_diff ( source , ancestor_path , patches , [ ] )
222+ end
223+
224+ defp do_map_diff ( [ ] , source , ancestor_path , patches , checked_keys ) do
207225 # The complete desination was check. Every key that is not in the list of
208226 # checked keys, must be removed.
209- source
210- |> flat ( )
211- |> Stream . map ( fn { k , _ } -> escape ( k ) end )
212- |> Stream . filter ( fn k -> k not in checked_keys end )
213- |> Stream . map ( fn k -> % { op: "remove" , path: "#{ ancestor_path } /#{ k } " } end )
214- |> Enum . reduce ( patches , fn remove_patch , patches -> [ remove_patch | patches ] end )
227+ Enum . reduce ( source , patches , fn { k , _ } , patches ->
228+ if k in checked_keys do
229+ patches
230+ else
231+ [ % { op: "remove" , path: "#{ ancestor_path } /#{ escape ( k ) } " } | patches ]
232+ end
233+ end )
215234 end
216235
217- defp do_diff ( [ { key , val } | tail ] , source , ancestor_path , patches , checked_keys ) do
218- current_path = "#{ ancestor_path } /#{ escape ( key ) } "
219-
236+ defp do_map_diff ( [ { key , val } | rest ] , source , ancestor_path , patches , checked_keys ) do
237+ # normal iteration through list of map {k, v} tuples. We track seen keys to later remove not seen keys.
220238 patches =
221- case Utils . fetch ( source , key ) do
222- # Key is not present in source
223- { :error , _ } ->
224- [ % { op: "add" , path: current_path , value: val } | patches ]
239+ case Map . fetch ( source , key ) do
240+ { :ok , source_val } -> do_diff ( val , source_val , ancestor_path , key , patches )
241+ :error -> [ % { op: "add" , path: " #{ ancestor_path } / #{ escape ( key ) } " , value: val } | patches ]
242+ end
225243
226- # Source has a different value but both (destination and source) value are lists or a maps
227- { :ok , source_val } when are_unequal_lists ( source_val , val ) ->
228- val |> flat ( ) |> Enum . reverse ( ) |> do_diff ( source_val , current_path , patches , [ ] )
244+ # Diff next value of same level
245+ do_map_diff ( rest , source , ancestor_path , patches , [ key | checked_keys ] )
246+ end
229247
230- { :ok , source_val } when are_unequal_maps ( source_val , val ) ->
231- # Enter next level - set check_keys to empty list because it is a different level
232- val |> flat ( ) |> do_diff ( source_val , current_path , patches , [ ] )
248+ defp do_list_diff ( destination , source , ancestor_path \\ "" , patches \\ [ ] , idx \\ 0 )
233249
234- # Scalar source val that is not equal
235- { :ok , source_val } when source_val != val ->
236- [ % { op: "replace" , path: current_path , value: val } | patches ]
250+ defp do_list_diff ( [ ] , [ ] , _path , patches , _idx ) , do: patches
237251
238- _ ->
239- patches
240- end
252+ defp do_list_diff ( [ ] , [ _item | source_rest ] , ancestor_path , patches , idx ) do
253+ # if we find any leftover items in source, we have to remove them
254+ patches = [ % { op: "remove" , path: "#{ ancestor_path } /#{ idx } " } | patches ]
255+ do_list_diff ( [ ] , source_rest , ancestor_path , patches , idx + 1 )
256+ end
241257
242- # Diff next value of same level
243- do_diff ( tail , source , ancestor_path , patches , [ escape ( key ) | checked_keys ] )
258+ defp do_list_diff ( items , [ ] , ancestor_path , patches , idx ) do
259+ # we have to do it without recursion, because we have to keep the order of the items
260+ items
261+ |> Enum . map_reduce ( idx , fn val , idx ->
262+ { % { op: "add" , path: "#{ ancestor_path } /#{ idx } " , value: val } , idx + 1 }
263+ end )
264+ |> elem ( 0 )
265+ |> Kernel . ++ ( patches )
266+ end
267+
268+ defp do_list_diff ( [ val | rest ] , [ source_val | source_rest ] , ancestor_path , patches , idx ) do
269+ # case when there's an item in both desitation and source. Let's just compare them
270+ patches = do_diff ( val , source_val , ancestor_path , idx , patches )
271+ do_list_diff ( rest , source_rest , ancestor_path , patches , idx + 1 )
244272 end
245273
246- # Transforms a map into a tuple list and a list also into a tuple list with indizes
247- defp flat ( val ) when is_list ( val ) ,
248- do: Stream . with_index ( val ) |> Enum . map ( fn { v , k } -> { k , v } end )
274+ @ compile { :inline , escape: 1 }
249275
250- defp flat ( val ) when is_map ( val ) ,
251- do: Map . to_list ( val )
276+ defp escape ( fragment ) when is_binary ( fragment ) do
277+ fragment =
278+ if :binary . match ( fragment , "~" ) != :nomatch ,
279+ do: String . replace ( fragment , "~" , "~0" ) ,
280+ else: fragment
281+
282+ if :binary . match ( fragment , "/" ) != :nomatch ,
283+ do: String . replace ( fragment , "/" , "~1" ) ,
284+ else: fragment
285+ end
252286
253- defp escape ( fragment ) when is_binary ( fragment ) , do: Utils . escape ( fragment )
254287 defp escape ( fragment ) , do: fragment
255288end
0 commit comments