Skip to main content

lib/container/scylla_container.ex

# SPDX-License-Identifier: MIT
defmodule TestcontainerEx.ScyllaContainer do
  @moduledoc """
  Provides functionality for creating and managing ScyllaDB container configurations.

  ScyllaDB is a Cassandra-compatible NoSQL database. This container exposes
  port 9042 (CQL) by default and uses `nodetool` to wait for readiness.

  ## Example

      {:ok, container} = TestcontainerEx.start_container(ScyllaContainer.new())
      port = ScyllaContainer.port(container)
      host = TestcontainerEx.get_host(container)
  """

  alias TestcontainerEx.CommandWaitStrategy
  alias TestcontainerEx.Container.Builder
  alias TestcontainerEx.Container.Config
  alias TestcontainerEx.ScyllaContainer

  use TestcontainerEx.ContainerConfig

  @default_image "scylladb/scylla"
  @default_tag "latest"
  @default_image_with_tag "#{@default_image}:#{@default_tag}"
  @default_cql_port 9042
  @default_wait_timeout 120_000

  @type t :: %__MODULE__{}

  @enforce_keys [:image, :wait_timeout]
  defstruct [
    :image,
    :wait_timeout,
    :name,
    check_image: @default_image,
    reuse: false
  ]

  # ── Constructor ────────────────────────────────────────────────────

  @doc """
  Creates a new ScyllaContainer with default configuration.
  """
  @spec new() :: t()
  def new,
    do: %__MODULE__{
      image: @default_image_with_tag,
      wait_timeout: @default_wait_timeout
    }

  # ── Setters ────────────────────────────────────────────────────────

  @doc """
  Sets the container image (e.g. `"scylladb/scylla:5.4"`).
  """
  @spec with_image(t(), String.t()) :: t()
  def with_image(%__MODULE__{} = config, image) when is_binary(image) do
    %{config | image: image}
  end

  @doc """
  Sets the wait timeout in milliseconds.
  """
  @spec with_wait_timeout(t(), pos_integer()) :: t()
  def with_wait_timeout(%__MODULE__{} = config, timeout)
      when is_integer(timeout) and timeout > 0 do
    %{config | wait_timeout: timeout}
  end

  @doc """
  Sets the container name.
  """
  @spec with_name(t(), String.t()) :: t()
  def with_name(%__MODULE__{} = config, name) when is_binary(name) do
    %__MODULE__{config | name: name}
  end

  # ── Accessors ──────────────────────────────────────────────────────

  @doc """
  Returns the default image name without tag.
  """
  @spec default_image() :: String.t()
  def default_image, do: @default_image

  @doc """
  Returns the default CQL port (9042).
  """
  @spec default_port() :: 9042
  def default_port, do: @default_cql_port

  @doc """
  Returns the mapped host port for the CQL port.
  """
  @spec port(Config.t()) :: integer() | nil
  def port(%Config{} = container), do: TestcontainerEx.get_port(container, @default_cql_port)

  @doc """
  Returns a `host:port` connection URI string.
  """
  @spec connection_uri(Config.t()) :: String.t()
  def connection_uri(%Config{} = container) do
    "#{TestcontainerEx.get_host(container)}:#{port(container)}"
  end

  # ── Builder protocol ───────────────────────────────────────────────

  defimpl Builder do
    @impl true
    @spec build(ScyllaContainer.t()) :: Config.t()
    def build(%ScyllaContainer{} = config) do
      Config.new(config.image)
      |> Config.with_exposed_port(ScyllaContainer.default_port())
      |> Config.with_cmd(["--smp", "1", "--memory", "1G"])
      |> Config.with_environment(:SCYLLA_SKIP_WAIT_FOR_GOSPEL_TO_SETTLE, "0")
      |> Config.with_waiting_strategy(
        CommandWaitStrategy.new(
          ["nodetool", "status"],
          config.wait_timeout
        )
      )
      |> Config.with_check_image(config.check_image)
      |> Config.with_reuse(config.reuse)
      |> then(fn cfg ->
        if config.name, do: Config.with_name(cfg, config.name), else: cfg
      end)
      |> Config.valid_image!()
    end

    @impl true
    def after_start(_config, _container, _conn), do: :ok
  end
end