lib/k8s/operation/path.ex

defmodule K8s.Operation.Path do
  @moduledoc "Generates Kubernetes REST API Paths"
  alias K8s.Operation.Error
  @path_params [:namespace, :name, :path, :logpath]

  @doc false
  @spec path_params() :: list(atom)
  def path_params, do: @path_params

  @doc """
  Generates the API path for a `K8s.Operation`.

  ## Examples

  Generate a path for a cluster scoped resource:

      iex> resource = K8s.Resource.build("apps/v1", "CertificateSigningRequest", "foo")
      ...> operation = %K8s.Operation{
      ...>  method: :put,
      ...>  verb: :update,
      ...>  data: resource,
      ...>  path_params: [name: "foo"],
      ...>  api_version: "apps/v1",
      ...>  name: "certificatesigningrequests"
      ...> }
      ...> K8s.Operation.Path.build(operation)
      {:ok, "/apis/apps/v1/certificatesigningrequests/foo"}


      iex> resource = K8s.Resource.build("apps/v1", "CertificateSigningRequest", "foo")
      ...> operation = %K8s.Operation{
      ...>  method: :put,
      ...>  verb: :update,
      ...>  data: resource,
      ...>  path_params: [name: "foo", namespace: nil],
      ...>  api_version: "apps/v1",
      ...>  name: "certificatesigningrequests"
      ...> }
      ...> K8s.Operation.Path.build(operation)
      {:ok, "/apis/apps/v1/certificatesigningrequests/foo"}

  Generate a path for a namespace scoped resource:

      iex> resource = K8s.Resource.build("v1", "Pod", "default", "foo")
      ...> operation = %K8s.Operation{
      ...>  method: :put,
      ...>  verb: :update,
      ...>  data: resource,
      ...>  path_params: [namespace: "default", name: "foo"],
      ...>  api_version: "v1",
      ...>  name: "pods"
      ...> }
      ...> K8s.Operation.Path.build(operation)
      {:ok, "/api/v1/namespaces/default/pods/foo"}

  Generate a path for a namespace scoped resource on the collection: (ie create, list)

      iex> resource = K8s.Resource.build("v1", "Pod", "default", "foo")
      ...> operation = %K8s.Operation{
      ...>  method: :post,
      ...>  verb: :create,
      ...>  data: resource,
      ...>  path_params: [namespace: "default", name: "foo"],
      ...>  api_version: "v1",
      ...>  name: "pods"
      ...> }
      ...> K8s.Operation.Path.build(operation)
      {:ok, "/api/v1/namespaces/default/pods"}

  Generating a listing path for a namespace:

      iex> operation = %K8s.Operation{
      ...>  method: :get,
      ...>  verb: :list,
      ...>  data: nil,
      ...>  path_params: [namespace: "default"],
      ...>  api_version: "v1",
      ...>  name: "pods"
      ...> }
      ...> K8s.Operation.Path.build(operation)
      {:ok, "/api/v1/namespaces/default/pods"}

  Generating a listing path for a all namespaces:

      iex> operation = %K8s.Operation{
      ...>  method: :get,
      ...>  verb: :list_all_namespaces,
      ...>  data: nil,
      ...>  path_params: [namespace: "default"],
      ...>  api_version: "v1",
      ...>  name: "pods"
      ...> }
      ...> K8s.Operation.Path.build(operation)
      {:ok, "/api/v1/pods"}

  Generating a listing path for a watching all namespaces:

      iex> operation = %K8s.Operation{
      ...>  method: :get,
      ...>  verb: :watch_all_namespaces,
      ...>  data: nil,
      ...>  path_params: [namespace: "default"],
      ...>  api_version: "v1",
      ...>  name: "pods"
      ...> }
      ...> K8s.Operation.Path.build(operation)
      {:ok, "/api/v1/pods"}

  Generating a path for a subresource:

      iex> operation = %K8s.Operation{
      ...>  method: :get,
      ...>  verb: :get,
      ...>  data: nil,
      ...>  path_params: [namespace: "default", name: "foo"],
      ...>  api_version: "v1",
      ...>  name: "pods/status"
      ...> }
      ...> K8s.Operation.Path.build(operation)
      {:ok, "/api/v1/namespaces/default/pods/foo/status"}

  Deleting a collection:

      iex> operation = %K8s.Operation{
      ...>  method: :delete,
      ...>  verb: :deletecollection,
      ...>  data: nil,
      ...>  path_params: [namespace: "default"],
      ...>  api_version: "v1",
      ...>  name: "pods"
      ...> }
      ...> K8s.Operation.Path.build(operation)
      {:ok, "/api/v1/namespaces/default/pods"}
  """
  @spec build(K8s.Operation.t()) :: {:ok, binary} | {:error, Error.t()}
  def build(%K8s.Operation{} = operation) do
    path_template = to_path(operation)
    required_params = K8s.Operation.Path.find_params(path_template)
    provided_params = Keyword.keys(operation.path_params)

    case required_params -- provided_params do
      [] ->
        {:ok, replace_path_vars(path_template, operation.path_params)}

      missing_params ->
        msg = "Missing required params: #{inspect(missing_params)}"
        {:error, %Error{message: msg}}
    end
  end

  @doc """
  Replaces path variables with options.

  ## Examples

      iex> K8s.Operation.Path.replace_path_vars("/foo/{name}", name: "bar")
      "/foo/bar"

  """
  @spec replace_path_vars(binary(), keyword()) :: binary()
  def replace_path_vars(path_template, opts) do
    Regex.replace(~r/\{(\w+?)\}/, path_template, fn _, var ->
      opts[String.to_existing_atom(var)]
    end)
  end

  @doc """
  Find valid path params in a URL path.

  ## Examples

      iex> K8s.Operation.Path.find_params("/foo/{name}")
      [:name]

      iex> K8s.Operation.Path.find_params("/foo/{namespace}/bar/{name}")
      [:namespace, :name]

      iex> K8s.Operation.Path.find_params("/foo/bar")
      []

  """
  @spec find_params(binary()) :: list(atom())
  def find_params(path_with_args) do
    ~r/{([a-z]+)}/
    |> Regex.scan(path_with_args)
    |> Enum.map(fn match -> match |> List.last() |> String.to_existing_atom() end)
  end

  @spec to_path(K8s.Operation.t()) :: binary
  defp to_path(%K8s.Operation{path_params: params} = operation) do
    has_namespace = !is_nil(params[:namespace])
    namespaced = has_namespace && params[:namespace] != :all

    prefix =
      case String.contains?(operation.api_version, "/") do
        true -> "/apis/#{operation.api_version}"
        false -> "/api/#{operation.api_version}"
      end

    suffix =
      operation.name
      |> String.split("/")
      |> resource_path_suffix(operation.verb)

    build_path(prefix, suffix, namespaced, operation.verb)
  end

  @spec resource_path_suffix(list(binary), atom) :: binary
  defp resource_path_suffix([name], verb), do: name_param(name, verb)

  defp resource_path_suffix([name, subresource], verb),
    do: name_with_subresource_param(name, subresource, verb)

  @spec name_param(binary, atom) :: binary
  defp name_param(resource_name, :create), do: resource_name
  defp name_param(resource_name, :list_all_namespaces), do: resource_name
  defp name_param(resource_name, :list), do: resource_name
  defp name_param(resource_name, :deletecollection), do: resource_name
  defp name_param(resource_name, :watch_all_namespaces), do: resource_name
  defp name_param(resource_name, _), do: "#{resource_name}/{name}"

  @spec name_with_subresource_param(binary, binary, atom) :: binary
  defp name_with_subresource_param(resource_name, subresource, _),
    do: "#{resource_name}/{name}/#{subresource}"

  @spec build_path(binary, binary, boolean, atom) :: binary
  defp build_path(prefix, suffix, true, :watch_all_namespaces), do: "#{prefix}/#{suffix}"
  defp build_path(prefix, suffix, true, :list_all_namespaces), do: "#{prefix}/#{suffix}"
  defp build_path(prefix, suffix, true, _), do: "#{prefix}/namespaces/{namespace}/#{suffix}"
  defp build_path(prefix, suffix, false, _), do: "#{prefix}/#{suffix}"
end