lib/bonny/api/resource_endpoint.ex

defmodule Bonny.API.ResourceEndpoint do
  @moduledoc """
  Defines the API Endpoint for a Kubernetes resource.

  The struct contains the fields `group`, `resource_type`, `scope` and `version`.
  New definitions can be created directly or using the `new!/1` function.
  """

  @typedoc """
  A Resource API Definition. Also see [Kubernetes API terminology](https://kubernetes.io/docs/reference/using-api/api-concepts/#standard-api-terminology).

  * `group`: The API group used for REST API: /apis/<group>/<version>, e.g. "apps" or "example.com"
  * `resource_type`: The plural form of the resource name
  * `scope`: `:Namespaced` or `:Cluster` - defaults to `:Namespaced`
  * `version`: The API version used for REST API: /apis/<group>/<version>, e.g. "v1" or "v1alpha1"
  """
  @type t :: %__MODULE__{
          group: binary() | nil,
          resource_type: binary(),
          scope: :Namespaced | :Cluster,
          version: binary()
        }

  defstruct [:group, :resource_type, :version, scope: :Namespaced]

  @doc """
  Creates a new %Bonny.API.ResourceEndpoint{} struct from the given values. `:scope` is
  optional and defaults to `:Namespaced`.
  """
  @spec new!(keyword()) :: __MODULE__.t()
  def new!(fields) do
    struct!(__MODULE__, fields)
  end

  @doc """
  Creates a new %Bonny.API.ResourceEndpoint{} struct from `apiVersion` and `kind`.
  The scope can be passed as a third optional parameter.

  ### Examples

      iex> Bonny.API.ResourceEndpoint.new!("apps/v1", "Deployment")
      %Bonny.API.ResourceEndpoint{group: "apps", resource_type: "deployments", scope: :Namespaced, version: "v1"}

      iex> Bonny.API.ResourceEndpoint.new!("v1", "Pod")
      %Bonny.API.ResourceEndpoint{group: nil, resource_type: "pods", scope: :Namespaced, version: "v1"}

      iex> Bonny.API.ResourceEndpoint.new!("rbac.authorization.k8s.io/v1", "ClusterRoleBinding", :Cluster)
      %Bonny.API.ResourceEndpoint{group: "rbac.authorization.k8s.io", resource_type: "clusterrolebindings", scope: :Cluster, version: "v1"}

      iex> Bonny.API.ResourceEndpoint.new!("foo/bar/v1", "ClusterRoleBinding", :Cluster)
      ** (ArgumentError) The api_version "foo/bar/v1" cannot be parsed. It contains more than one slash (/).
  """
  @spec new!(binary(), binary(), :Namespaced | :Cluster) :: t()
  def new!(api_version, kind, scope \\ :Namespaced) do
    resource_type = kind |> String.downcase() |> Inflex.pluralize()

    {group, version} =
      case String.split(api_version, "/") do
        [group, version] ->
          {group, version}

        [version] ->
          {nil, version}

        _ ->
          raise ArgumentError,
            message:
              "The api_version #{inspect(api_version)} cannot be parsed. It contains more than one slash (/)."
      end

    struct!(
      __MODULE__,
      group: group,
      resource_type: resource_type,
      scope: scope,
      version: version
    )
  end

  @doc """
  Gets apiVersion of the actual resources.

  ## Examples
    Returns apiVersion for an operator

      iex> Bonny.API.ResourceEndpoint.resource_api_version(%Bonny.API.ResourceEndpoint{group: "hello.example.com", version: "v1", scope: :namespaced, resource_type: "foos"})
      "hello.example.com/v1"

    Returns apiVersion for `apps` resources

      iex> Bonny.API.ResourceEndpoint.resource_api_version(%Bonny.API.ResourceEndpoint{group: "apps", version: "v1", scope: :namespaced, resource_type: "foos"})
      "apps/v1"

    Returns apiVersion for `core` resources

      iex> Bonny.API.ResourceEndpoint.resource_api_version(%Bonny.API.ResourceEndpoint{group: "", version: "v1", scope: :namespaced, resource_type: "foos"})
      "v1"

      iex> Bonny.API.ResourceEndpoint.resource_api_version(%Bonny.API.ResourceEndpoint{group: nil, version: "v1", scope: :namespaced, resource_type: "foos"})
      "v1"
  """
  @spec resource_api_version(t()) :: String.t()
  def resource_api_version(definition),
    do: api_group_prefix(definition) <> definition.version

  defp api_group_prefix(%__MODULE__{group: ""}), do: ""
  defp api_group_prefix(%__MODULE__{group: nil}), do: ""
  defp api_group_prefix(%__MODULE__{group: g}), do: "#{g}/"
end