├── .formatter.exs ├── .gitignore ├── .velocity.yml ├── LICENSE.txt ├── README.md ├── config └── config.exs ├── lib └── json_diff.ex ├── mix.exs ├── mix.lock ├── tasks └── exunit-test.yml └── test ├── json_diff_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | fast_json_diff_ex-*.tar 24 | 25 | .elixir_ls 26 | -------------------------------------------------------------------------------- /.velocity.yml: -------------------------------------------------------------------------------- 1 | project: 2 | logo: //i.imgur.com/MUTLB8Q.png 3 | tasksPath: ./tasks 4 | git: 5 | depth: 10 -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Eddy Lane. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSONDiff 2 | 3 | JSONDiff is an Elixir implementation of the diffing element of the JSON Patch format, 4 | described in [RFC 6902](http://tools.ietf.org/html/rfc6902). 5 | 6 | This library only handles diffing. For patching, see the wonderful [JSONPatch library](https://github.com/gamache/json_patch_elixir). 7 | 8 | This library only supports add, replace and remove operations. 9 | 10 | It is based on the very fast JavaScript library [JSON-Patch](https://github.com/Starcounter-Jack/JSON-Patch) 11 | 12 | ## Examples 13 | 14 | iex> JSONDiff.diff(%{"a" => 1}, %{"a" => 2}) 15 | [%{"op" => "replace", "path" => "/a", "value" => 2}] 16 | 17 | iex> JSONDiff.diff([1], [2]) 18 | [%{"op" => "replace", "path" => "/0", "value" => 2}] 19 | 20 | ## Installation 21 | 22 | # mix.exs 23 | def deps do 24 | [ 25 | {:json_diff, "~> 0.1.2"} 26 | ] 27 | end 28 | 29 | ## Authorship and License 30 | 31 | JSONDiff is released under the MIT License, available at LICENSE.txt. 32 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :fast_json_diff_ex, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:fast_json_diff_ex, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/json_diff.ex: -------------------------------------------------------------------------------- 1 | defmodule JSONDiff do 2 | @moduledoc ~S""" 3 | JSONDiff is an Elixir implementation of the diffing element of the JSON Patch format, 4 | described in [RFC 6902](http://tools.ietf.org/html/rfc6902). 5 | 6 | This library only handles diffing. For patching, see the wonderful [JSONPatch library](https://github.com/gamache/json_patch_elixir). 7 | 8 | This library only supports add, replace and remove operations. 9 | 10 | It is based on the very fast JavaScript library [JSON-Patch](https://github.com/Starcounter-Jack/JSON-Patch) 11 | 12 | ## Examples 13 | iex> JSONDiff.diff(%{"a" => 1}, %{"a" => 2}) 14 | [%{"op" => "replace", "path" => "/a", "value" => 2}] 15 | iex> JSONDiff.diff([1], [2]) 16 | [%{"op" => "replace", "path" => "/0", "value" => 2}] 17 | 18 | ## Installation 19 | # mix.exs 20 | def deps do 21 | [ 22 | {:json_diff, "~> 0.1.0"} 23 | ] 24 | end 25 | """ 26 | 27 | def diff(old, new, patches \\ [], path \\ "") do 28 | {deleted, patches, old_keys} = patches_for_old(old, new, patches, path) 29 | new_keys = list_or_map_keys_or_indexes(new) 30 | 31 | unless deleted or length(new_keys) != length(old_keys) do 32 | patches 33 | end 34 | 35 | Enum.reduce(new_keys, patches, fn key, patches -> 36 | if !has_key_or_index?(old, key) do 37 | add_patch(patches, patch(:add, path, key, item(new, key))) 38 | else 39 | patches 40 | end 41 | end) 42 | end 43 | 44 | @doc false 45 | defp patches_for_old(old, new, patches, path) do 46 | old_keys = 47 | list_or_map_keys_or_indexes(old) 48 | |> Enum.reverse() 49 | 50 | {deleted, patches} = Enum.reduce(old_keys, {false, patches}, old_key_reducer(old, new, path)) 51 | 52 | {deleted, patches, old_keys} 53 | end 54 | 55 | @doc false 56 | defp old_key_reducer(old, new, path) do 57 | fn key, {deleted, patches} -> 58 | old_val = item(old, key) 59 | 60 | case has_key_or_index?(new, key) do 61 | # The key for the old exists in the new 62 | true -> 63 | new_val = item(new, key) 64 | 65 | cond do 66 | # Both are maps or lists, so we need to recurse to check the child values 67 | map_or_list?(old_val) and map_or_list?(new_val) -> 68 | child_patches = 69 | diff( 70 | old_val, 71 | new_val, 72 | [], 73 | path_component(path, key) 74 | ) 75 | 76 | patches = add_patch(patches, child_patches) 77 | {deleted || false, patches} 78 | 79 | # No changes, do nothing 80 | new_val === old_val -> 81 | {deleted || false, patches} 82 | 83 | # Changes, replace old value with new value 84 | true -> 85 | patches = add_patch(patches, patch(:replace, path, key, new_val)) 86 | {deleted || false, patches} 87 | end 88 | 89 | # The key for the old does not exist in the new 90 | false -> 91 | patches = add_patch(patches, patch(:remove, path, key)) 92 | {true, patches} 93 | end 94 | end 95 | end 96 | 97 | @doc false 98 | defp add_patch(patches, new_patches) when is_list(new_patches), do: patches ++ new_patches 99 | defp add_patch(patches, new_patch), do: patches ++ [new_patch] 100 | 101 | @doc false 102 | defp has_key_or_index?(map, key) when is_map(map) and is_binary(key) do 103 | Map.has_key?(map, key) 104 | end 105 | 106 | defp has_key_or_index?(map, key) when is_map(map) and is_atom(key) do 107 | Map.has_key?(map, key) 108 | end 109 | 110 | defp has_key_or_index?(list, index) when is_list(list) and is_integer(index) do 111 | Enum.at(list, index) != nil 112 | end 113 | 114 | defp has_key_or_index?(_, _), do: false 115 | 116 | @doc false 117 | defp patch(:add, path, key, val) do 118 | %{ 119 | "op" => "add", 120 | "path" => path_component(path, key), 121 | "value" => val 122 | } 123 | end 124 | 125 | defp patch(:replace, path, key, val) do 126 | %{ 127 | "op" => "replace", 128 | "path" => path_component(path, key), 129 | "value" => val 130 | } 131 | end 132 | 133 | defp patch(:remove, path, key) do 134 | %{ 135 | "op" => "remove", 136 | "path" => path_component(path, key) 137 | } 138 | end 139 | 140 | @doc false 141 | defp path_component(path, key), do: path <> "/" <> escape_path_component(key) 142 | 143 | @doc false 144 | defp list_or_map_keys_or_indexes(map) when is_map(map), do: Map.keys(map) 145 | 146 | defp list_or_map_keys_or_indexes([] = list) when is_list(list), do: [] 147 | 148 | defp list_or_map_keys_or_indexes(list) when is_list(list) do 149 | 0..(length(list) - 1) 150 | |> Enum.to_list() 151 | end 152 | 153 | @doc false 154 | defp map_or_list?(%NaiveDateTime{}), do: false 155 | defp map_or_list?(%DateTime{}), do: false 156 | defp map_or_list?(%Date{}), do: false 157 | defp map_or_list?(a) when is_list(a) or is_map(a), do: true 158 | defp map_or_list?(_), do: false 159 | 160 | @doc false 161 | defp item(enum, key) when is_map(enum), do: Map.fetch!(enum, key) 162 | 163 | defp item(enum, index) when is_list(enum), do: Enum.fetch!(enum, index) 164 | 165 | @doc false 166 | def escape_path_component(path) when is_integer(path) do 167 | path 168 | |> to_string() 169 | |> escape_path_component() 170 | end 171 | 172 | def escape_path_component(path) when is_binary(path) do 173 | case {:binary.match(path, "/"), :binary.match(path, "~")} do 174 | {:nomatch, :nomatch} -> 175 | path 176 | 177 | _ -> 178 | path = Regex.replace(~r/~/, path, "~0") 179 | Regex.replace(~r/\//, path, "~1") 180 | end 181 | end 182 | 183 | def escape_path_component(path) when is_atom(path) do 184 | escape_path_component(Atom.to_string(path)) 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule JSONDiff.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :json_diff, 7 | description: "An Elixir implementation of the diffing element of JSON Patch (RFC 6902)", 8 | version: "0.1.3", 9 | elixir: "~> 1.6", 10 | package: package(), 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps() 13 | ] 14 | end 15 | 16 | defp package do 17 | [ 18 | files: ["lib", "mix.exs", "README.md"], 19 | maintainers: ["Eddy Lane "], 20 | licenses: ["MIT"], 21 | links: %{ 22 | "GitHub" => "https://github.com/EddyLane/elixir_json_diff", 23 | "Docs" => "https://hexdocs.pm/json_diff" 24 | } 25 | ] 26 | end 27 | 28 | # Run "mix help compile.app" to learn about applications. 29 | def application do 30 | [ 31 | extra_applications: [:logger] 32 | ] 33 | end 34 | 35 | # Run "mix help deps" to learn about dependencies. 36 | defp deps do 37 | [ 38 | {:ex_doc, "~> 0.20.1", only: :dev}, 39 | {:json_patch, "~> 0.8.0", only: :test} 40 | ] 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm", "e3be2bc3ae67781db529b80aa7e7c49904a988596e2dbff897425b48b3581161"}, 3 | "ex_doc": {:hex, :ex_doc, "0.20.1", "88eaa16e67c505664fd6a66f42ddb962d424ad68df586b214b71443c69887123", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "704b758b659a143f2f1b6f20cc3db2107a899c86c8485458ae2fcb0773651e59"}, 4 | "json_patch": {:hex, :json_patch, "0.8.0", "45c9fdaceb49739af3ab60196e2f2cc89b6e832e1ff2c1accf80432108b86777", [:mix], [], "hexpm", "ecc7c4a72388ae8907ccf3ec10f604da992214d5e535c18ba531009b1a77d0c7"}, 5 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5fbc8e549aa9afeea2847c0769e3970537ed302f93a23ac612602e805d9d1e7f"}, 6 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "adf0218695e22caeda2820eaba703fa46c91820d53813a2223413da3ef4ba515"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm", "5c040b8469c1ff1b10093d3186e2e10dbe483cd73d79ec017993fb3985b8a9b3"}, 8 | } 9 | -------------------------------------------------------------------------------- /tasks/exunit-test.yml: -------------------------------------------------------------------------------- 1 | description: "Runs ExUnit tests on this repository" 2 | name: exunit-test 3 | 4 | steps: 5 | - type: run 6 | description: ExUnit 7 | image: elixir:1.6 8 | command: /bin/sh -c "mix local.hex --force && mix deps.get && mix test --color" 9 | mountPoint: /app -------------------------------------------------------------------------------- /test/json_diff_test.exs: -------------------------------------------------------------------------------- 1 | defmodule JSONDiffTest do 2 | use ExUnit.Case 3 | doctest JSONDiff 4 | 5 | import JSONDiff 6 | 7 | describe "duplex" do 8 | test "it should generate replace" do 9 | a = %{ 10 | "firstName" => "Albert", 11 | "lastName" => "Einstein", 12 | "phoneNumbers" => [ 13 | %{"number" => "12345"}, 14 | %{"number" => "45353"} 15 | ] 16 | } 17 | 18 | b = %{ 19 | "firstName" => "Joachim", 20 | "lastName" => "Wester", 21 | "phoneNumbers" => [ 22 | %{"number" => "123"}, 23 | %{"number" => "456"} 24 | ] 25 | } 26 | 27 | expected_patches = [ 28 | %{ 29 | "op" => "replace", 30 | "path" => "/phoneNumbers/1/number", 31 | "value" => "456" 32 | }, 33 | %{ 34 | "op" => "replace", 35 | "path" => "/phoneNumbers/0/number", 36 | "value" => "123" 37 | }, 38 | %{"op" => "replace", "path" => "/lastName", "value" => "Wester"}, 39 | %{"op" => "replace", "path" => "/firstName", "value" => "Joachim"} 40 | ] 41 | 42 | patches = diff(a, b) 43 | assert patches == expected_patches 44 | 45 | assert {:ok, ^b} = JSONPatch.patch(a, patches) 46 | end 47 | 48 | test "it should generate replace (escaped chars)" do 49 | a = %{ 50 | "/name/first" => "Albert", 51 | "/name/last" => "Einstein", 52 | "~phone~/numbers" => [ 53 | %{"number" => "12345"}, 54 | %{"number" => "45353"} 55 | ] 56 | } 57 | 58 | b = %{ 59 | "/name/first" => "Joachim", 60 | "/name/last" => "Wester", 61 | "~phone~/numbers" => [ 62 | %{"number" => "123"}, 63 | %{"number" => "456"} 64 | ] 65 | } 66 | 67 | expected_patches = [ 68 | %{ 69 | "op" => "replace", 70 | "path" => "/~0phone~0~1numbers/1/number", 71 | "value" => "456" 72 | }, 73 | %{ 74 | "op" => "replace", 75 | "path" => "/~0phone~0~1numbers/0/number", 76 | "value" => "123" 77 | }, 78 | %{"op" => "replace", "path" => "/~1name~1last", "value" => "Wester"}, 79 | %{"op" => "replace", "path" => "/~1name~1first", "value" => "Joachim"} 80 | ] 81 | 82 | patches = diff(a, b) 83 | 84 | assert patches == expected_patches 85 | assert {:ok, ^b} = JSONPatch.patch(a, patches) 86 | end 87 | end 88 | 89 | test "it should generate replace (changes in new array cell, primitive values)" do 90 | a = [1] 91 | b = [1, 2] 92 | 93 | patches = diff(a, b) 94 | 95 | assert patches == [%{"op" => "add", "path" => "/1", "value" => 2}] 96 | assert {:ok, ^b} = JSONPatch.patch(a, patches) 97 | 98 | c = [3, 2] 99 | 100 | patches = diff(b, c) 101 | assert patches == [%{"op" => "replace", "path" => "/0", "value" => 3}] 102 | assert {:ok, ^c} = JSONPatch.patch(b, patches) 103 | 104 | d = [3, 4] 105 | 106 | patches = diff(c, d) 107 | assert patches == [%{"op" => "replace", "path" => "/1", "value" => 4}] 108 | assert {:ok, ^d} = JSONPatch.patch(c, patches) 109 | end 110 | 111 | test "it should generate replace (changes in new array cell, complex values)" do 112 | a = [ 113 | %{ 114 | "id" => 1, 115 | "name" => "Ted" 116 | } 117 | ] 118 | 119 | b = 120 | a ++ 121 | [ 122 | %{ 123 | "id" => 2, 124 | "name" => "Jerry" 125 | } 126 | ] 127 | 128 | patches = diff(a, b) 129 | 130 | assert patches == [ 131 | %{ 132 | "op" => "add", 133 | "path" => "/1", 134 | "value" => %{ 135 | "id" => 2, 136 | "name" => "Jerry" 137 | } 138 | } 139 | ] 140 | 141 | assert {:ok, ^b} = JSONPatch.patch(a, patches) 142 | end 143 | 144 | test "it should generate replace for NaiveDateTime" do 145 | now_a = NaiveDateTime.utc_now() 146 | now_b = NaiveDateTime.add(now_a, 1, :minute) 147 | 148 | a = %{"now" => now_a} 149 | b = %{"now" => now_b} 150 | 151 | result = [ 152 | %{ 153 | "op" => "replace", 154 | "path" => "/now", 155 | "value" => now_b 156 | } 157 | ] 158 | 159 | patches = diff(a, b) 160 | 161 | assert patches == result 162 | end 163 | 164 | test "it should generate replace for DateTime" do 165 | now_a = DateTime.utc_now() 166 | now_b = DateTime.add(now_a, 1, :minute) 167 | 168 | a = %{"now" => now_a} 169 | b = %{"now" => now_b} 170 | 171 | result = [ 172 | %{ 173 | "op" => "replace", 174 | "path" => "/now", 175 | "value" => now_b 176 | } 177 | ] 178 | 179 | patches = diff(a, b) 180 | 181 | assert patches == result 182 | end 183 | 184 | test "it should generate replace for Date" do 185 | now_a = Date.utc_today() 186 | now_b = Date.add(now_a, 1) 187 | 188 | a = %{"now" => now_a} 189 | b = %{"now" => now_b} 190 | 191 | result = [ 192 | %{ 193 | "op" => "replace", 194 | "path" => "/now", 195 | "value" => now_b 196 | } 197 | ] 198 | 199 | patches = diff(a, b) 200 | 201 | assert patches == result 202 | end 203 | 204 | test "it should generate add" do 205 | a = %{ 206 | "lastName" => "Einstein", 207 | "phoneNumbers" => [ 208 | %{"number" => "12345"} 209 | ] 210 | } 211 | 212 | b = %{ 213 | "firstName" => "Joachim", 214 | "lastName" => "Wester", 215 | "phoneNumbers" => [ 216 | %{"number" => "123"}, 217 | %{"number" => "456"} 218 | ] 219 | } 220 | 221 | patches = diff(a, b) 222 | 223 | assert patches == [ 224 | %{"op" => "replace", "path" => "/phoneNumbers/0/number", "value" => "123"}, 225 | %{"op" => "add", "path" => "/phoneNumbers/1", "value" => %{"number" => "456"}}, 226 | %{"op" => "replace", "path" => "/lastName", "value" => "Wester"}, 227 | %{"op" => "add", "path" => "/firstName", "value" => "Joachim"} 228 | ] 229 | 230 | assert {:ok, ^b} = JSONPatch.patch(a, patches) 231 | end 232 | 233 | test "it should generate remove" do 234 | a = %{ 235 | "lastName" => "Einstein", 236 | "firstName" => "Albert", 237 | "phoneNumbers" => [ 238 | %{"number" => "12345"}, 239 | %{"number" => "4234"} 240 | ] 241 | } 242 | 243 | b = %{ 244 | "lastName" => "Wester", 245 | "phoneNumbers" => [ 246 | %{"number" => "123"} 247 | ] 248 | } 249 | 250 | patches = diff(a, b) 251 | 252 | assert patches == [ 253 | %{"op" => "remove", "path" => "/phoneNumbers/1"}, 254 | %{"op" => "replace", "path" => "/phoneNumbers/0/number", "value" => "123"}, 255 | %{"op" => "replace", "path" => "/lastName", "value" => "Wester"}, 256 | %{"op" => "remove", "path" => "/firstName"} 257 | ] 258 | 259 | assert {:ok, ^b} = JSONPatch.patch(a, patches) 260 | end 261 | 262 | test "it should generate remove (list indexes should be sorted descending)" do 263 | a = %{ 264 | "items" => ["a", "b", "c"] 265 | } 266 | 267 | b = %{ 268 | "items" => ["a"] 269 | } 270 | 271 | patches = diff(a, b) 272 | 273 | # array indexes must be sorted descending, otherwise there is an index collision in apply 274 | assert patches == [ 275 | %{"op" => "remove", "path" => "/items/2"}, 276 | %{"op" => "remove", "path" => "/items/1"} 277 | ] 278 | 279 | assert {:ok, ^b} = JSONPatch.patch(a, patches) 280 | end 281 | 282 | test "it should generate add for a list" do 283 | a = %{ 284 | "a" => [] 285 | } 286 | 287 | b = %{ 288 | "a" => [1] 289 | } 290 | 291 | patches = diff(a, b) 292 | 293 | assert patches == [ 294 | %{"op" => "add", "path" => "/a/0", "value" => 1} 295 | ] 296 | 297 | assert {:ok, ^b} = JSONPatch.patch(a, patches) 298 | end 299 | 300 | test "it should diff for atom keys" do 301 | a = %{a: 1} 302 | b = %{a: 2} 303 | 304 | patches = diff(a, b) 305 | assert patches == [%{"op" => "replace", "path" => "/a", "value" => 2}] 306 | 307 | # When using atom keys, it cannot be patched successfully 308 | assert {:error, _error, _desc} = JSONPatch.patch(a, patches) 309 | end 310 | end 311 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------