Current section
Files
Jump to
Current section
Files
lib/json_patch.ex
defmodule JSONPatch do
@moduledoc ~S"""
JSONPatch is an Elixir implementation of the JSON Patch format,
described in [RFC 6902](http://tools.ietf.org/html/rfc6902).
## Examples
iex> JSONPatch.patch(%{"a" => 1}, [
...> %{"op" => "add", "path" => "/b", "value" => %{"c" => true}},
...> %{"op" => "test", "path" => "/a", "value" => 1},
...> %{"op" => "move", "from" => "/b/c", "path" => "/c"}
...> ])
{:ok, %{"a" => 1, "b" => %{}, "c" => true}}
iex> JSONPatch.patch(%{"a" => 22}, [
...> %{"op" => "add", "path" => "/b", "value" => %{"c" => true}},
...> %{"op" => "test", "path" => "/a", "value" => 1},
...> %{"op" => "move", "from" => "/b/c", "path" => "/c"}
...> ])
{:error, :test_failed, ~s|test failed (patches[1], %{"op" => "test", "path" => "/a", "value" => 1})|}
## Installation
# mix.exs
def deps do
[
{:json_patch, "~> 0.8.0"}
]
end
"""
alias JSONPatch.Path
@type json_document :: json_object | json_array
@type json_object :: %{String.t() => json_encodable}
@type json_array :: [json_encodable]
@type json_encodable ::
json_object
| json_array
| String.t()
| number
| true
| false
| nil
@type patches :: [patch]
@type patch :: map
@type return_value :: {:ok, json_encodable} | {:error, error_type, String.t()}
@type error_type :: :test_failed | :syntax_error | :path_error
@type status_code :: non_neg_integer
@doc ~S"""
Applies JSON Patch (RFC 6902) patches to the given JSON document.
Returns `{:ok, patched_map}` or `{:error, error_type, description}`.
Examples:
iex> %{"foo" => "bar"} |> JSONPatch.patch([%{"op" => "replace", "path" => "/foo", "value" => 2}])
{:ok, %{"foo" => 2}}
iex> %{"foo" => "bar"} |> JSONPatch.patch([%{"op" => "test", "path" => "/foo", "value" => 2}])
{:error, :test_failed, ~s|test failed (patches[0], %{"op" => "test", "path" => "/foo", "value" => 2})|}
iex> %{"foo" => "bar"} |> JSONPatch.patch([%{"op" => "remove", "path" => "/foo"}])
{:ok, %{}}
"""
@spec patch(json_document, patches, non_neg_integer) :: return_value
def patch(doc, patches) do
patch(doc, patches, 0)
end
defp patch(doc, [], _), do: {:ok, doc}
defp patch(doc, [p | rest], i) do
case apply_single_patch(doc, p) do
{:ok, newdoc} ->
patch(newdoc, rest, i + 1)
{:error, type, desc} ->
{:error, type, "#{desc} (patches[#{i}], #{inspect(p)})"}
end
end
@doc ~S"""
Converts a `t:return_value/0` to an HTTP status code.
Example:
iex> JSONPatch.patch(%{"a" => 1}, [%{"op" => "test", "path" => "/a", "value" => 1}]) |> JSONPatch.status_code
200
iex> JSONPatch.patch(%{"a" => 1}, [%{"op" => "test", "path" => "/a", "value" => 22}]) |> JSONPatch.status_code
409
"""
@spec status_code(return_value) :: status_code
def status_code(return_value)
def status_code({:ok, _}), do: 200
def status_code({:error, type, _}) do
error_type_to_status_code(type)
end
@doc ~S"""
Converts an `t:error_type/0` into an HTTP status code.
Examples:
iex> JSONPatch.error_type_to_status_code(:test_failed)
409
iex> JSONPatch.error_type_to_status_code(:path_error)
422
iex> JSONPatch.error_type_to_status_code(:syntax_error)
400
"""
@spec error_type_to_status_code(error_type) :: status_code
def error_type_to_status_code(error_type) do
case error_type do
:test_failed -> 409
:path_error -> 422
:syntax_error -> 400
_ -> 400
end
end
@spec apply_single_patch(json_document, patch) :: return_value
defp apply_single_patch(doc, patch) 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)
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
end
end
defp apply_op("remove", doc, patch) do
Path.remove_value_at_path(doc, patch["path"])
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"])
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
Path.add_value_at_path(data, patch["path"], patch["value"])
else
err -> err
end
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
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
Path.add_value_at_path(doc, patch["path"], value)
else
err -> err
end
end
end
defp apply_op(op, _doc, _patch) do
{:error, :syntax_error, "not implemented: #{op}"}
end
end