@@ -10,7 +10,6 @@ defmodule Jsonpatch do
1010 by using `~1` for `/` and `~0` for `~`.
1111 """
1212
13- alias Jsonpatch.FlatMap
1413 alias Jsonpatch.Operation
1514 alias Jsonpatch.Operation.Add
1615 alias Jsonpatch.Operation.Copy
@@ -104,88 +103,122 @@ defmodule Jsonpatch do
104103 end
105104
106105 @ doc """
107- Creates a patch from the difference of a source map to a target map.
106+ Creates a patch from the difference of a source map to a destination map or list .
108107
109108 ## Examples
110109
111110 iex> source = %{"name" => "Bob", "married" => false, "hobbies" => ["Elixir", "Sport", "Football"]}
112111 iex> destination = %{"name" => "Bob", "married" => true, "hobbies" => ["Elixir!"], "age" => 33}
113112 iex> Jsonpatch.diff(source, destination)
114113 [
115- %Add {path: "/age ", value: 33 },
116- %Replace {path: "/hobbies/0", value: "Elixir! "},
117- %Replace {path: "/married", value: true },
118- %Remove {path: "/hobbies/1 "},
119- %Remove {path: "/hobbies/2" }
114+ %Jsonpatch.Operation.Replace {path: "/married ", value: true },
115+ %Jsonpatch.Operation.Remove {path: "/hobbies/2 "},
116+ %Jsonpatch.Operation.Remove {path: "/hobbies/1" },
117+ %Jsonpatch.Operation.Replace {path: "/hobbies/0", value: "Elixir! "},
118+ %Jsonpatch.Operation.Add {path: "/age", value: 33 }
120119 ]
121120 """
122- @ spec diff ( map , map ) :: list ( Jsonpatch . t ( ) )
121+ @ spec diff ( maybe_improper_list | map , maybe_improper_list | map ) :: list ( Jsonpatch . t ( ) )
123122 def diff ( source , destination )
124123
125124 def diff ( % { } = source , % { } = destination ) do
126- source = FlatMap . parse ( source )
127- destination = FlatMap . parse ( destination )
125+ Map . to_list ( destination )
126+ |> do_diff ( source , "" )
127+ end
128+
129+ def diff ( source , destination ) when is_list ( source ) and is_list ( destination ) do
130+ Enum . with_index ( destination )
131+ |> Enum . map ( fn { v , k } -> { k , v } end )
132+ |> do_diff ( source , "" )
133+ end
128134
135+ def diff ( _ , _ ) do
129136 [ ]
130- |> create_additions ( source , destination )
131- |> create_replaces ( source , destination )
132- |> create_removes ( source , destination )
133137 end
134138
135- @ doc """
136- Creates "add"-operations by using the keys of the destination and check their existence in the
137- source map. Source and destination has to be parsed to a flat map.
138- """
139- @ spec create_additions ( list ( Jsonpatch . t ( ) ) , map , map ) :: list ( Jsonpatch . t ( ) )
140- def create_additions ( accumulator \\ [ ] , source , destination )
141-
142- def create_additions ( accumulator , % { } = source , % { } = destination ) do
143- additions =
144- Map . keys ( destination )
145- |> Enum . filter ( fn key -> not Map . has_key? ( source , key ) end )
146- |> Enum . map ( fn key ->
147- % Add { path: key , value: Map . get ( destination , key ) }
148- end )
149-
150- accumulator ++ additions
139+ # ===== ===== PRIVATE ===== =====
140+
141+ # Helper for better readability
142+ defguardp are_unequal_maps ( val1 , val2 )
143+ when val1 != val2 and is_map ( val2 ) and is_map ( val1 )
144+
145+ # Helper for better readability
146+ defguardp are_unequal_lists ( val1 , val2 )
147+ when val1 != val2 and is_list ( val2 ) and is_list ( val1 )
148+
149+ # Diff reduce loop
150+ defp do_diff ( destination , source , ancestor_path , acc \\ [ ] , checked_keys \\ [ ] )
151+
152+ defp do_diff ( [ ] , source , ancestor_path , acc , checked_keys ) do
153+ # The complete desination was check. Every key that is not in the list of
154+ # checked keys, must be removed.
155+ acc =
156+ source
157+ |> flat ( )
158+ |> Stream . map ( fn { k , _ } -> k end )
159+ |> Stream . filter ( fn k -> k not in checked_keys end )
160+ |> Stream . map ( fn k -> % Remove { path: "#{ ancestor_path } /#{ k } " } end )
161+ |> Enum . reduce ( acc , fn r , acc -> [ r | acc ] end )
162+
163+ acc
151164 end
152165
153- @ doc """
154- Creates "remove"-operations by using the keys of the destination and check their existence in the
155- source map. Source and destination has to be parsed to a flat map.
156- """
157- @ spec create_removes ( list ( Jsonpatch . t ( ) ) , map , map ) :: list ( Jsonpatch . t ( ) )
158- def create_removes ( accumulator \\ [ ] , source , destination )
166+ defp do_diff ( [ { key , val } | tail ] , source , ancestor_path , acc , checked_keys )
167+ when is_list ( source ) or is_map ( source ) do
168+ current_path = "#{ ancestor_path } /#{ escape ( key ) } "
169+
170+ acc =
171+ case get ( source , key ) do
172+ # Key is not present in source
173+ nil ->
174+ [ % Add { path: current_path , value: val } | acc ]
159175
160- def create_removes ( accumulator , % { } = source , % { } = destination ) do
161- removes =
162- Map . keys ( source )
163- |> Enum . filter ( fn key -> not Map . has_key? ( destination , key ) end )
164- |> Enum . map ( fn key -> % Remove { path: key } end )
176+ # Source has a different value but both (destination and source) value are lists or a maps
177+ source_val
178+ when are_unequal_lists ( source_val , val ) or are_unequal_maps ( source_val , val ) ->
179+ # Enter next level - set check_keys to empty list because it is a different level
180+ do_diff ( flat ( val ) , source_val , current_path , acc , [ ] )
181+
182+ # Scalar source val that is not equal
183+ source_val when source_val != val ->
184+ [ % Replace { path: current_path , value: val } | acc ]
185+
186+ _ ->
187+ acc
188+ end
165189
166- accumulator ++ removes
190+ # Diff next value of same level
191+ do_diff ( tail , source , ancestor_path , acc , [ escape ( key ) | checked_keys ] )
167192 end
168193
169- @ doc """
170- Creates "replace"-operations by comparing keys and values of source and destination. The source and
171- destination map have to be flat maps.
172- """
173- @ spec create_replaces ( list ( Jsonpatch . t ( ) ) , map , map ) :: list ( Jsonpatch . t ( ) )
174- def create_replaces ( accumulator \\ [ ] , source , destination )
175-
176- def create_replaces ( accumulator , source , destination ) do
177- replaces =
178- Map . keys ( destination )
179- |> Enum . filter ( fn key -> Map . has_key? ( source , key ) end )
180- |> Enum . filter ( fn key -> Map . get ( source , key ) != Map . get ( destination , key ) end )
181- |> Enum . map ( fn key ->
182- % Replace { path: key , value: Map . get ( destination , key ) }
183- end )
184-
185- accumulator ++ replaces
194+ # Transforms a map into a tuple list and a list also into a tuple list with indizes
195+ defp flat ( val ) when is_list ( val ) do
196+ Stream . with_index ( val ) |> Enum . map ( fn { v , k } -> { k , v } end )
186197 end
187198
188- # ===== ===== PRIVATE ===== =====
199+ defp flat ( val ) when is_map ( val ) do
200+ Map . to_list ( val )
201+ end
202+
203+ # Unified access to lists or maps
204+ defp get ( source , key ) when is_list ( source ) do
205+ Enum . at ( source , key )
206+ end
207+
208+ defp get ( source , key ) do
209+ Map . get ( source , key )
210+ end
211+
212+ # Escape `/` to `~1 and `~` to `~`.
213+ defp escape ( subpath ) when is_bitstring ( subpath ) do
214+ subpath
215+ |> String . replace ( "~" , "~0" )
216+ |> String . replace ( "/" , "~1" )
217+ end
218+
219+ defp escape ( subpath ) do
220+ subpath
221+ end
189222
190223 # Create once a easy sortable value for a operation
191224 defp create_sort_value ( % { path: path } = operation ) do
0 commit comments