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.apply_op(remove, target)
      %{"a" => %{}, "d" => false}
  """

  alias Jsonpatch.Operation
  alias Jsonpatch.Operation.Remove
  alias Jsonpatch.PathUtil

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

  defimpl Operation do
    @spec apply_op(Remove.t(), list() | map() | Jsonpatch.error(), keyword()) ::
            map()
    def apply_op(_, {:error, _, _} = error, _opts), do: error

    def apply_op(%Remove{path: path}, target, opts) do
      key_type = PathUtil.wanted_key_type(opts)

      # The first element is always "" which is useless.
      [_ | fragments] =
        path
        |> String.split("/")
        |> Enum.map(&PathUtil.unescape/1)
        |> PathUtil.into_key_type(key_type)

      do_remove(target, fragments)
    end

    # ===== ===== PRIVATE ===== =====

    defp do_remove(%{} = target, [fragment | []]) do
      case Map.pop(target, fragment) do
        {nil, _} -> {:error, :invalid_path, fragment}
        {_, purged_map} -> purged_map
      end
    end

    defp do_remove(target, [fragment | []]) when is_list(target) do
      case Integer.parse(fragment) do
        :error ->
          {:error, :invalid_index, fragment}

        {index, _} ->
          case List.pop_at(target, index) do
            {nil, _} -> {:error, :invalid_index, fragment}
            {_, purged_list} -> purged_list
          end
      end
    end

    defp do_remove(%{} = target, [fragment | tail]) do
      case Map.get(target, fragment) do
        nil ->
          {:error, :invalid_path, fragment}

        val ->
          case do_remove(val, tail) do
            {:error, _, _} = error -> error
            new_val -> %{target | fragment => new_val}
          end
      end
    end

    defp do_remove(target, [fragment | tail]) when is_list(target) do
      case Integer.parse(fragment) do
        :error ->
          {:error, :invalid_index, fragment}

        {index, _} ->
          PathUtil.update_at(target, index, tail, &do_remove/2)
      end
    end
  end
end