lib/momento/internal/scs_control_client.ex

defmodule Momento.Internal.ScsControlClient do
  import Momento.Validation

  alias Momento.Auth.CredentialProvider
  alias Momento.Responses.{CacheInfo, CreateCache, DeleteCache, ListCaches}

  @moduledoc false

  @enforce_keys [:auth_token, :channel]
  defstruct [:auth_token, :channel]

  @opaque t() :: %__MODULE__{
            auth_token: String.t(),
            channel: GRPC.Channel.t()
          }

  defimpl Inspect, for: Momento.Internal.ScsControlClient do
    def inspect(%Momento.Internal.ScsControlClient{} = control_client, _opts) do
      "#Momento.Internal.ScsControlClient<auth_token: [hidden], channel: #{inspect(control_client.channel)}>"
    end
  end

  @spec create(CredentialProvider.t()) :: {:ok, t()} | {:error, any()}
  def create(credential_provider) do
    control_endpoint = CredentialProvider.control_endpoint(credential_provider)
    tls_options = :tls_certificate_check.options(control_endpoint)

    with {:ok, channel} <-
           GRPC.Stub.connect(control_endpoint <> ":443",
             cred: GRPC.Credential.new(ssl: tls_options)
           ) do
      {:ok,
       %__MODULE__{
         auth_token: CredentialProvider.auth_token(credential_provider),
         channel: channel
       }}
    end
  end

  @spec list_caches(client :: t()) :: Momento.Responses.ListCaches.t()
  def list_caches(client) do
    metadata = create_metadata(client)
    list_caches_request = %Momento.Protos.ControlClient.ListCachesRequest{}

    case Momento.Protos.ControlClient.ScsControl.Stub.list_caches(
           client.channel,
           list_caches_request,
           metadata: metadata
         ) do
      {:ok, response} ->
        {:ok,
         %ListCaches.Ok{
           caches: Enum.map(response.cache, fn c -> %CacheInfo{name: c.cache_name} end)
         }}

      {:error, error_response} ->
        {:error, Momento.Error.convert(error_response)}
    end
  end

  @spec create_cache(client :: t(), cache_name :: String.t()) :: Momento.Responses.CreateCache.t()
  def create_cache(client, cache_name) do
    metadata = create_metadata(client)

    create_cache_request = %Momento.Protos.ControlClient.CreateCacheRequest{
      cache_name: cache_name
    }

    with :ok <- validate_cache_name(cache_name) do
      case Momento.Protos.ControlClient.ScsControl.Stub.create_cache(
             client.channel,
             create_cache_request,
             metadata: metadata
           ) do
        {:ok, _} ->
          {:ok, %CreateCache.Ok{}}

        {:error, error_response} ->
          err = Momento.Error.convert(error_response)

          case err.error_code do
            :already_exists_error -> :already_exists
            _ -> {:error, err}
          end
      end
    end
  end

  @spec delete_cache(client :: t(), cache_name :: String.t()) :: Momento.Responses.DeleteCache.t()
  def delete_cache(client, cache_name) do
    metadata = create_metadata(client)

    delete_cache_request = %Momento.Protos.ControlClient.DeleteCacheRequest{
      cache_name: cache_name
    }

    with :ok <- validate_cache_name(cache_name) do
      case Momento.Protos.ControlClient.ScsControl.Stub.delete_cache(
             client.channel,
             delete_cache_request,
             metadata: metadata
           ) do
        {:ok, _} ->
          {:ok, %DeleteCache.Ok{}}

        {:error, error_response} ->
          {:error, Momento.Error.convert(error_response)}
      end
    end
  end

  @agent_data_key "__#{__MODULE__}_AGENT_DATA_SENT__"

  defp should_send_agent_data? do
    :erlang.get(@agent_data_key) == :undefined
  end

  @spec create_metadata(t()) :: %{required(String.t()) => String.t()}
  defp create_metadata(client) do
    base_metadata = %{
      "authorization" => client.auth_token
    }

    if should_send_agent_data?() do
      :erlang.put(@agent_data_key, true)

      Map.merge(base_metadata, %{
        # example agent: "elixir:cache:0.6.6"
        "agent" => "elixir:cache:" <> get_library_version(),
        # example runtime-version: "1.16.2"
        "runtime-version" => System.version()
      })
    else
      base_metadata
    end
  end

  @spec get_library_version() :: String.t()
  defp get_library_version do
    case Application.spec(:gomomento, :vsn) do
      nil -> "unknown"
      version -> to_string(version)
    end
  end
end