lib/k8s/client/dynamic_http_provider.ex

defmodule K8s.Client.DynamicHTTPProvider do
  @moduledoc """
  Allows for registration of `K8s.Client.Provider` handlers per-process.

  Used internally by the test suite for testing/mocking Kubernetes responses.
  """
  use GenServer
  @behaviour K8s.Client.Provider

  @doc "Starts this provider."
  @spec start_link(any) :: GenServer.on_start()
  def start_link(_) do
    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  @doc "Locate the handler module for this process or any ancestor"
  @spec locate(pid) :: module() | function() | nil
  def locate(nil), do: nil

  def locate(pid) do
    case GenServer.call(__MODULE__, {:locate, pid}) do
      nil ->
        pid
        |> Process.info(:links)
        |> elem(1)
        |> List.first()
        |> locate()

      handler ->
        handler
    end
  end

  @doc "List all registered handlers"
  @spec list :: map()
  def list, do: GenServer.call(__MODULE__, :list)

  @doc "Register the handler mdoule for this process"
  @spec register(pid(), module() | function()) :: map()
  def register(this_pid, module_or_function) do
    GenServer.call(__MODULE__, {:register, this_pid, module_or_function})
  end

  @doc """
  Dispatch `request/5` to the module registered in the current process or any ancestor.
  """
  @impl true
  def request(method, url, body, headers, opts) do
    locate_and_apply(:request, [method, url, body, headers, opts])
  end

  @doc """
  Dispatch `stream_to/6` to the module registered in the current process or any ancestor.
  """
  @impl true
  def stream(method, url, body, headers, opts) do
    locate_and_apply(:stream, [method, url, body, headers, opts])
  end

  @doc """
  Dispatch `stream_to/6` to the module registered in the current process or any ancestor.
  """
  @impl true
  def stream_to(method, url, body, headers, opts, stream_to) do
    locate_and_apply(:stream_to, [method, url, body, headers, opts, stream_to])
  end

  @doc """
  Dispatch `request/5` to the module registered in the current process or any ancestor.
  """
  @impl true
  def websocket_request(url, headers, opts) do
    locate_and_apply(:websocket_request, [url, headers, opts])
  end

  @doc """
  Dispatch `request/5` to the module registered in the current process or any ancestor.
  """
  @impl true
  def websocket_stream(url, headers, opts) do
    locate_and_apply(:websocket_stream, [url, headers, opts])
  end

  @doc """
  Dispatch `request/5` to the module registered in the current process or any ancestor.
  """
  @impl true
  def websocket_stream_to(url, headers, opts, stream_to) do
    locate_and_apply(:websocket_stream_to, [url, headers, opts, stream_to])
  end

  @spec locate_and_apply(atom(), list()) :: K8s.Client.Provider.response_t()
  defp locate_and_apply(func, args) do
    case locate(self()) do
      nil ->
        raise "No handler module registered for process #{inspect(self())} or parent."

      module when is_atom(module) ->
        apply(module, func, args)

      callback when is_function(callback) ->
        apply(callback, args)
    end
  end

  @impl true
  def init(:ok) do
    {:ok, %{}}
  end

  @impl true
  def handle_call(:list, _from, state) do
    {:reply, state, state}
  end

  @impl true
  def handle_call({:locate, this_pid}, _from, pids) do
    {:reply, Map.get(pids, this_pid), pids}
  end

  @impl true
  def handle_call({:register, this_pid, module}, _from, state) do
    new_state = Map.put(state, this_pid, module)
    {:reply, new_state, new_state}
  end
end