diff --git a/config/config.exs b/config/config.exs index 0ff4a46..23fd9f1 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,30 +1 @@ -# This file is responsible for configuring your application -# and its dependencies with the aid of the Mix.Config module. -use Mix.Config - -# This configuration is loaded before any dependency and is restricted -# to this project. If another project depends on this project, this -# file won't be loaded nor affect the parent project. For this reason, -# if you want to provide default values for your application for -# 3rd-party users, it should be done in your "mix.exs" file. - -# You can configure your application as: -# -# config :json_patch, key: :value -# -# and access this configuration in your application as: -# -# Application.get_env(:json_patch, :key) -# -# You can also configure a 3rd-party app: -# -# config :logger, level: :info -# - -# It is also possible to import configuration files, relative to this -# directory. For example, you can emulate configuration per environment -# by uncommenting the line below and defining dev.exs, test.exs and such. -# Configuration from the imported file will override the ones defined -# here (which is why it is important to import them last). -# -# import_config "#{Mix.env}.exs" +import Mix.Config diff --git a/lib/json_patch.ex b/lib/json_patch.ex index 13f5f1d..2321add 100644 --- a/lib/json_patch.ex +++ b/lib/json_patch.ex @@ -1,5 +1,5 @@ defmodule JSONPatch do - @moduledoc ~S""" + @moduledoc """ JSONPatch is an Elixir implementation of the JSON Patch format, described in [RFC 6902](http://tools.ietf.org/html/rfc6902). @@ -57,7 +57,7 @@ defmodule JSONPatch do @type status_code :: non_neg_integer - @doc ~S""" + @doc """ Applies JSON Patch (RFC 6902) patches to the given JSON document. Returns `{:ok, patched_map}` or `{:error, error_type, description}`. @@ -89,7 +89,7 @@ defmodule JSONPatch do end end - @doc ~S""" + @doc """ Converts a `t:return_value/0` or `t:error_type/0' to an HTTP status code. The HTTP status codes emitted are: @@ -126,28 +126,26 @@ defmodule JSONPatch do cond do !Map.has_key?(patch, "op") -> {:error, :syntax_error, "missing `op`"} !Map.has_key?(patch, "path") -> {:error, :syntax_error, "missing `path`"} - :else -> apply_op(patch["op"], doc, patch) + true -> apply_op(patch["op"], doc, patch) end end @spec apply_op(String.t(), json_document, patch) :: return_value defp apply_op("test", doc, patch) do - cond do - !Map.has_key?(patch, "value") -> - {:error, :syntax_error, "missing `value`"} - - :else -> - case Path.get_value_at_path(doc, patch["path"]) do - {:ok, path_value} -> - if path_value == patch["value"] do - {:ok, doc} - else - {:error, :test_failed, "test failed"} - end - - err -> - err - end + if Map.has_key?(patch, "value") do + case Path.get_value_at_path(doc, patch["path"]) do + {:ok, path_value} -> + if path_value == patch["value"] do + {:ok, doc} + else + {:error, :test_failed, "test failed"} + end + + err -> + err + end + else + {:error, :syntax_error, "missing `value`"} end end @@ -156,55 +154,49 @@ defmodule JSONPatch do end defp apply_op("add", doc, patch) do - cond do - !Map.has_key?(patch, "value") -> - {:error, :syntax_error, "missing `value`"} - - :else -> - Path.add_value_at_path(doc, patch["path"], patch["value"]) + if Map.has_key?(patch, "value") do + Path.add_value_at_path(doc, patch["path"], patch["value"]) + else + {:error, :syntax_error, "missing `value`"} end end defp apply_op("replace", doc, patch) do - cond do - !Map.has_key?(patch, "value") -> - {:error, :syntax_error, "missing `value`"} - - :else -> - with {:ok, data} <- Path.remove_value_at_path(doc, patch["path"]) do + if Map.has_key?(patch, "value") do + case Path.remove_value_at_path(doc, patch["path"]) do + {:ok, data} -> Path.add_value_at_path(data, patch["path"], patch["value"]) - else - err -> err - end + + err -> + err + end + else + {:error, :syntax_error, "missing `value`"} end end defp apply_op("move", doc, patch) do - cond do - !Map.has_key?(patch, "from") -> - {:error, :syntax_error, "missing `from`"} - - :else -> - with {:ok, value} <- Path.get_value_at_path(doc, patch["from"]), - {:ok, data} <- Path.remove_value_at_path(doc, patch["from"]) do - Path.add_value_at_path(data, patch["path"], value) - else - err -> err - end + with true <- Map.has_key?(patch, "from"), + {:ok, value} <- Path.get_value_at_path(doc, patch["from"]), + {:ok, data} <- Path.remove_value_at_path(doc, patch["from"]) do + Path.add_value_at_path(data, patch["path"], value) + else + false -> {:error, :syntax_error, "missing `from`"} + err -> err end end defp apply_op("copy", doc, patch) do - cond do - !Map.has_key?(patch, "from") -> - {:error, :syntax_error, "missing `from`"} - - :else -> - with {:ok, value} <- Path.get_value_at_path(doc, patch["from"]) do + if Map.has_key?(patch, "from") do + case Path.get_value_at_path(doc, patch["from"]) do + {:ok, value} -> Path.add_value_at_path(doc, patch["path"], value) - else - err -> err - end + + err -> + err + end + else + {:error, :syntax_error, "missing `from`"} end end diff --git a/lib/json_patch/path.ex b/lib/json_patch/path.ex index d6ebc94..e372fb6 100644 --- a/lib/json_patch/path.ex +++ b/lib/json_patch/path.ex @@ -1,7 +1,7 @@ defmodule JSONPatch.Path do @moduledoc false - @doc ~S""" + @doc """ Splits a JSON Pointer (RFC 6901) path into its components. Path keys are converted to integers if possible, otherwise remaining strings. @@ -17,13 +17,21 @@ defmodule JSONPatch.Path do @spec split_path(String.t()) :: [String.t() | non_neg_integer] def split_path(path) + def split_path(nil), do: {:error, :path_error, "null is not valid value for 'path'"} + def split_path(""), do: [] def split_path(path) do - path - |> String.replace_leading("/", "") - |> String.split("/") - |> Enum.map(&convert_number/1) + case path do + "/" <> _remainder -> + path + |> String.replace_leading("/", "") + |> String.split("/") + |> Enum.map(&convert_number/1) + + _otherwise -> + {:error, :path_error, "JSON Pointer should start with a slash"} + end end ## Converts string-formatted integers back to integers. @@ -40,7 +48,7 @@ defmodule JSONPatch.Path do end end - @doc ~S""" + @doc """ Traverses `data` according to the given `path`, returning `{:ok, value}` if a value was found at that path, or `{:error, reason}` otherwise. """ @@ -62,7 +70,7 @@ defmodule JSONPatch.Path do defp value_at_path(data, ["-" | rest]) when is_list(data) do case Enum.count(data) do 0 -> {:error, :path_error, "can't use index '-' with empty array"} - c -> value_at_path(data, [c-1 | rest]) + c -> value_at_path(data, [c - 1 | rest]) end end @@ -81,7 +89,7 @@ defmodule JSONPatch.Path do {:error, :path_error, "can't index into value #{data}"} end - @doc ~S""" + @doc """ Attempts to remove the value at the given path. Returns the updated `{:ok, data}`, otherwise `{:error, reason}. @@ -120,7 +128,7 @@ defmodule JSONPatch.Path do defp remove_at_path(data, ["-" | rest]) when is_list(data) do case Enum.count(data) do 0 -> {:error, :path_error, "can't use index '-' with empty array"} - c -> remove_at_path(data, [c-1 | rest]) + c -> remove_at_path(data, [c - 1 | rest]) end end @@ -131,14 +139,14 @@ defmodule JSONPatch.Path do defp remove_at_path(%{} = data, [key | rest]) do keystr = to_string(key) - if !Map.has_key?(data, keystr) do - {:error, :path_error, "missing key #{keystr}"} - else + if Map.has_key?(data, keystr) do case remove_at_path(data[keystr], rest) do {:ok, :removed} -> {:ok, Map.delete(data, keystr)} {:ok, value} -> {:ok, Map.put(data, keystr, value)} err -> err end + else + {:error, :path_error, "missing key #{keystr}"} end end @@ -146,7 +154,7 @@ defmodule JSONPatch.Path do {:error, :path_error, "can't index into value #{data}"} end - @doc ~S""" + @doc """ Attempts to add the value at the given path. Returns the updated `{:ok, data}`, otherwise `{:error, reason}. @@ -176,11 +184,13 @@ defmodule JSONPatch.Path do rest == [] -> {:ok, List.insert_at(data, key, value)} - :else -> - with {:ok, v} <- add_at_path(Enum.at(data, key), rest, value) do - {:ok, List.replace_at(data, key, v)} - else - err -> err + true -> + case add_at_path(Enum.at(data, key), rest, value) do + {:ok, v} -> + {:ok, List.replace_at(data, key, v)} + + err -> + err end end end @@ -196,20 +206,20 @@ defmodule JSONPatch.Path do defp add_at_path(%{} = data, [key | rest], value) do keystr = to_string(key) - cond do - rest == [] -> - {:ok, Map.put(data, keystr, value)} - - :else -> - with {:ok, v} <- add_at_path(data[keystr], rest, value) do + if rest == [] do + {:ok, Map.put(data, keystr, value)} + else + case add_at_path(data[keystr], rest, value) do + {:ok, v} -> {:ok, Map.put(data, keystr, v)} - else - err -> err - end + + err -> + err + end end end defp add_at_path(data, _, _) do - {:error, :path_error, "can't index into value #{data}"} + {:error, :path_error, "can't index into value #{inspect(data)}"} end end diff --git a/mix.exs b/mix.exs index a308dba..17a7dce 100644 --- a/mix.exs +++ b/mix.exs @@ -55,9 +55,10 @@ defmodule JSONPatch.MixProject do [ {:dialyxir, "~> 0.5", only: :dev}, {:ex_doc, "~> 0.18", only: :dev}, - {:jason, "~> 1.0", only: :test}, + {:jason, "~> 1.0", only: [:test, :dev]}, {:ex_spec, "~> 2.0", only: :test}, {:excoveralls, "~> 0.8", only: :test}, + {:credo, "~> 1.7", only: [:test, :dev]} ] end end diff --git a/mix.lock b/mix.lock index 990e129..c6e4956 100644 --- a/mix.lock +++ b/mix.lock @@ -1,17 +1,15 @@ %{ - "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, - "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.18.2", "993e0a95e9fbb790ac54ea58e700b45b299bd48bc44b4ae0404f28161f37a83e", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, - "ex_spec": {:hex, :ex_spec, "2.0.1", "8bdbd6fa85995fbf836ed799571d44be6f9ebbcace075209fd0ad06372c111cf", [:mix], [], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.8.1", "0bbf67f22c7dbf7503981d21a5eef5db8bbc3cb86e70d3798e8c802c74fa5e27", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, - "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, + "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, + "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm", "6c32a70ed5d452c6650916555b1f96c79af5fc4bf286997f8b15f213de786f73"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, + "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, + "ex_spec": {:hex, :ex_spec, "2.0.1", "8bdbd6fa85995fbf836ed799571d44be6f9ebbcace075209fd0ad06372c111cf", [:mix], [], "hexpm", "b44fe5054497411a58341ece5bf7756c219d9d6c1303b5ac467f557a0a4c31ac"}, + "excoveralls": {:hex, :excoveralls, "0.18.3", "bca47a24d69a3179951f51f1db6d3ed63bca9017f476fe520eb78602d45f7756", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "746f404fcd09d5029f1b211739afb8fb8575d775b21f6a3908e7ce3e640724c6"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, } diff --git a/test/json_patch_test.exs b/test/json_patch_test.exs index f2ff41e..2701ca6 100644 --- a/test/json_patch_test.exs +++ b/test/json_patch_test.exs @@ -12,32 +12,34 @@ defmodule JSONPatchTest do "json-patch-tests/spec_tests.json" ] for filename <- @test_suites do - with {:ok, text} <- "./test/#{filename}" |> File.read() do - tests = text |> Jason.decode!() + case "./test/#{filename}" |> File.read() do + {:ok, text} -> + tests = text |> Jason.decode!() - for {t, i} <- Enum.with_index(tests) do - test "#{filename}[#{i}] (#{t["comment"]})" do - tt = unquote(Macro.escape(t)) + for {t, i} <- Enum.with_index(tests) do + test "#{filename}[#{i}] (#{t["comment"]})" do + tt = unquote(Macro.escape(t)) - cond do - tt["disabled"] -> - :skipped + cond do + tt["disabled"] -> + :skipped - tt["error"] -> - assert({:error, type, desc} = JSONPatch.patch(tt["doc"], tt["patch"])) - if tt["error_type"] do - assert(String.to_existing_atom(tt["error_type"]) == type, desc) - end + tt["error"] -> + assert({:error, type, desc} = JSONPatch.patch(tt["doc"], tt["patch"])) - tt["expected"] -> - assert({:ok, tt["expected"]} == JSONPatch.patch(tt["doc"], tt["patch"])) + if tt["error_type"] do + assert(String.to_existing_atom(tt["error_type"]) == type, desc) + end - :else -> - assert(JSONPatch.patch(tt["doc"], tt["patch"])) + tt["expected"] -> + assert({:ok, tt["expected"]} == JSONPatch.patch(tt["doc"], tt["patch"])) + + :else -> + assert(JSONPatch.patch(tt["doc"], tt["patch"])) + end end end - end - else + _ -> IO.puts( "Test suite #{filename} not present -- run `mix download-tests` to download test suite"