lib/jsonpatch/operation/remove.ex

defmodule Jsonpatch.Operation.Remove do
  @moduledoc """
  A JSON patch remove operation is responsible for removing values.

  ## Examples

      iex> remove = %Jsonpatch.Operation.Remove{path: "/a/b"}
      iex> target = %{"a" => %{"b" => %{"c" => "Bob"}}, "d" => false}
      iex> Jsonpatch.Operation.Remove.apply(remove, target, [])
      {:ok, %{"a" => %{}, "d" => false}}

      iex> remove = %Jsonpatch.Operation.Remove{path: "/a/b"}
      iex> target = %{"a" => %{"b" => nil}, "d" => false}
      iex> Jsonpatch.Operation.Remove.apply(remove, target, [])
      {:ok, %{"a" => %{}, "d" => false}}
  """

  alias Jsonpatch.Operation.Remove
  alias Jsonpatch.Types
  alias Jsonpatch.Utils

  @enforce_keys [:path]
  defstruct [:path]
  @type t :: %__MODULE__{path: String.t()}

  @spec apply(Jsonpatch.t(), target :: Types.json_container(), Types.opts()) ::
          {:ok, Types.json_container()} | Types.error()
  def apply(%Remove{path: path}, target, opts) do
    with {:ok, fragments} <- Utils.split_path(path) do
      do_remove(target, [], fragments, opts)
    end
  end

  defp do_remove(%{} = target, path, [fragment], opts) do
    with {:ok, fragment} <- Utils.cast_fragment(fragment, path, target, opts),
         %{^fragment => _} <- target do
      {:ok, Map.delete(target, fragment)}
    else
      %{} ->
        {:error, {:invalid_path, path ++ [fragment]}}

      # coveralls-ignore-start
      {:error, _} = error ->
        error
        # coveralls-ignore-stop
    end
  end

  defp do_remove(%{} = target, path, [fragment | tail], opts) do
    with {:ok, fragment} <- Utils.cast_fragment(fragment, path, target, opts),
         %{^fragment => val} <- target,
         {:ok, new_val} <- do_remove(val, path ++ [fragment], tail, opts) do
      {:ok, %{target | fragment => new_val}}
    else
      %{} -> {:error, {:invalid_path, path ++ [fragment]}}
      {:error, _} = error -> error
    end
  end

  defp do_remove(target, path, [fragment | tail], opts) when is_list(target) do
    case Utils.cast_fragment(fragment, path, target, opts) do
      {:ok, :-} ->
        {:error, {:invalid_path, path ++ [fragment]}}

      {:ok, index} ->
        if tail == [] do
          {:ok, List.delete_at(target, index)}
        else
          Utils.update_at(target, index, path, &do_remove(&1, path ++ [fragment], tail, opts))
        end

      {:error, _} = error ->
        error
    end
  end

  defp do_remove(_target, path, [fragment | _], _opts) do
    {:error, {:invalid_path, path ++ [fragment]}}
  end
end