Skip to main content

lib/kameleoon/client.ex

defmodule Kameleoon.Client do
  @moduledoc """
  Elixir wrapper over a native Kameleoon client Rustler resource.
  """

  alias Kameleoon.Error
  alias Kameleoon.CookieAccessor
  alias Kameleoon.Native.Events
  alias Kameleoon.Native.Nif
  alias Kameleoon.Types.DataFile
  alias Kameleoon.Types.RemoteVisitorDataFilter
  alias Kameleoon.Types.Variation
  alias Kameleoon.Internal.GetVisitorCodeResult
  alias Kameleoon.Internal.NativeResult

  defstruct [:native, :site_code, :environment, events: Events]

  @type t :: %__MODULE__{
          native: term(),
          site_code: String.t() | nil,
          environment: String.t() | nil,
          events: GenServer.server()
        }

  @native_callback_timeout 30_000
  @native_reply_tag :kameleoon_native

  @spec initialize(t(), keyword()) :: :ok | {:error, Error.t()}
  def initialize(client, opts \\ []) do
    do_initialize(client, opts)
  end

  @spec is_ready?(t()) :: boolean()
  def is_ready?(client) do
    Nif.is_ready(client.native)
  end

  @spec get_visitor_code(t(), CookieAccessor.t(), String.t() | nil) ::
          {:ok, String.t(), CookieAccessor.state()} | {:error, Error.t()}
  def get_visitor_code(client, cookies, default_visitor_code \\ nil)

  def get_visitor_code(client, cookies, default_visitor_code) do
    request_cookies = CookieAccessor.request_cookies(cookies)

    case native_get_visitor_code(client, request_cookies, default_visitor_code) do
      {:ok, %GetVisitorCodeResult{visitor_code: visitor_code, cookies: cookie_payload}} ->
        {:ok, visitor_code, CookieAccessor.apply_response_cookies(cookies, cookie_payload)}

      error ->
        error
    end
  end

  @spec set_legal_consent(t(), String.t(), boolean(), CookieAccessor.t() | nil) ::
          {:ok, CookieAccessor.state() | nil} | {:error, Error.t()}
  def set_legal_consent(client, visitor_code, consent, cookies \\ nil) do
    request_cookies = if cookies, do: CookieAccessor.request_cookies(cookies)

    case native_set_legal_consent(client, visitor_code, consent, request_cookies) do
      {:ok, cookie_payload} ->
        {:ok, cookies && CookieAccessor.apply_response_cookies(cookies, cookie_payload)}

      error ->
        error
    end
  end

  @spec add_data(t(), String.t(), struct() | [struct()], keyword()) :: :ok | {:error, Error.t()}
  def add_data(client, visitor_code, data, opts \\ []) do
    NativeResult.ok(Nif.add_data(client.native, visitor_code, List.wrap(data), Keyword.get(opts, :track, true)))
  end

  @spec flush(t(), String.t()) :: :ok | {:error, Error.t()}
  def flush(client, visitor_code) do
    NativeResult.ok(Nif.flush(client.native, visitor_code))
  end

  @spec flush_instant(t(), String.t()) :: :ok | {:error, Error.t()}
  def flush_instant(client, visitor_code) do
    with {:ok, ref} <- start_flush_instant(client, visitor_code) do
      await_native_ok(:ok, ref)
    end
  end

  @spec track_conversion(t(), String.t(), non_neg_integer(), keyword()) ::
          :ok | {:error, Error.t()}
  def track_conversion(client, visitor_code, goal_id, opts \\ []) do
    NativeResult.ok(
      Nif.track_conversion(
        client.native,
        visitor_code,
        goal_id,
        Keyword.get(opts, :revenue),
        Keyword.get(opts, :negative, false),
        Keyword.get(opts, :metadata)
      )
    )
  end

  @spec is_feature_active?(t(), String.t(), String.t(), keyword()) ::
          {:ok, boolean()} | {:error, Error.t()}
  def is_feature_active?(client, visitor_code, feature_key, opts \\ []) do
    NativeResult.result(
      Nif.is_feature_active(
        client.native,
        visitor_code,
        feature_key,
        Keyword.get(opts, :track, true)
      )
    )
  end

  @spec get_variation(t(), String.t(), String.t(), keyword()) ::
          {:ok, Variation.t()} | {:error, Error.t()}
  def get_variation(client, visitor_code, feature_key, opts \\ []) do
    NativeResult.result(
      Nif.get_variation(
        client.native,
        visitor_code,
        feature_key,
        Keyword.get(opts, :track, true)
      )
    )
  end

  @spec get_variations(t(), String.t(), keyword()) ::
          {:ok, %{String.t() => Variation.t()}} | {:error, Error.t()}
  def get_variations(client, visitor_code, opts \\ []) do
    NativeResult.result(
      Nif.get_variations(
        client.native,
        visitor_code,
        Keyword.get(opts, :only_active, false),
        Keyword.get(opts, :track, true)
      )
    )
  end

  @spec set_forced_variation(t(), String.t(), non_neg_integer(), String.t() | nil, keyword()) ::
          :ok | {:error, Error.t()}
  def set_forced_variation(client, visitor_code, experiment_id, variation_key \\ nil, opts \\ []) do
    NativeResult.ok(
      Nif.set_forced_variation(
        client.native,
        visitor_code,
        experiment_id,
        variation_key,
        Keyword.get(opts, :force_targeting, true)
      )
    )
  end

  @spec evaluate_audiences(t(), String.t()) :: :ok | {:error, Error.t()}
  def evaluate_audiences(client, visitor_code) do
    NativeResult.ok(Nif.evaluate_audiences(client.native, visitor_code))
  end

  @spec get_engine_tracking_code(t(), String.t()) ::
          {:ok, String.t()} | {:error, Error.t()}
  def get_engine_tracking_code(client, visitor_code) do
    NativeResult.result(Nif.get_engine_tracking_code(client.native, visitor_code))
  end

  @spec get_remote_data(t(), String.t()) :: {:ok, String.t()} | {:error, Error.t()}
  def get_remote_data(client, key) do
    with {:ok, ref} <- start_get_remote_data(client, key) do
      await(ref)
    end
  end

  @spec get_remote_visitor_data(t(), String.t(), keyword()) :: :ok | {:error, Error.t()}
  def get_remote_visitor_data(client, visitor_code, opts \\ []) do
    with {:ok, ref} <- start_get_remote_visitor_data(client, visitor_code, remote_visitor_data_filter(opts)) do
      await_native_ok(:ok, ref)
    end
  end

  @spec get_visitor_warehouse_audience(t(), String.t(), non_neg_integer(), keyword()) ::
          :ok | {:error, Error.t()}
  def get_visitor_warehouse_audience(client, visitor_code, custom_data_index, opts \\ []) do
    with {:ok, ref} <-
           start_get_visitor_warehouse_audience(
             client,
             visitor_code,
             custom_data_index,
             Keyword.get(opts, :warehouse_key)
           ) do
      await_native_ok(:ok, ref)
    end
  end

  @spec get_datafile(t()) :: {:ok, DataFile.t()} | {:error, Error.t()}
  def get_datafile(client) do
    NativeResult.result(Nif.get_datafile(client.native))
  end

  @spec on_datafile_update(t(), (-> any()) | nil) :: :ok | {:error, Error.t()}
  def on_datafile_update(client, fun) when is_function(fun, 0) or is_nil(fun) do
    Events.set_handler(client, fun, client.events)
  end

  defp native_get_visitor_code(client, request_cookies, default_visitor_code) do
    NativeResult.result(Nif.get_visitor_code(client.native, request_cookies, default_visitor_code))
  end

  defp native_set_legal_consent(client, visitor_code, consent, request_cookies) do
    NativeResult.result(Nif.set_legal_consent(client.native, visitor_code, consent, request_cookies))
  end

  defp do_initialize(client, opts) do
    with {:ok, ref} <- start_initialize(client, opts) do
      await_native_ok(:ok, ref)
    end
  end

  defp start_initialize(client, opts) do
    ref = make_ref()

    client.native
    |> Nif.initialize(Keyword.get(opts, :timeout), self(), ref)
    |> NativeResult.ref(ref)
  end

  defp start_flush_instant(client, visitor_code) do
    ref = make_ref()

    client.native
    |> Nif.flush_instant(visitor_code, self(), ref)
    |> NativeResult.ref(ref)
  end

  defp start_get_remote_data(client, key) do
    ref = make_ref()

    client.native
    |> Nif.get_remote_data(key, self(), ref)
    |> NativeResult.ref(ref)
  end

  defp start_get_remote_visitor_data(client, visitor_code, filter) do
    ref = make_ref()

    client.native
    |> Nif.get_remote_visitor_data(visitor_code, filter, self(), ref)
    |> NativeResult.ref(ref)
  end

  defp remote_visitor_data_filter(opts) do
    case Keyword.get(opts, :filter) do
      %RemoteVisitorDataFilter{} = filter -> filter
      nil -> nil
    end
  end

  defp start_get_visitor_warehouse_audience(
         client,
         visitor_code,
         custom_data_index,
         warehouse_key
       ) do
    ref = make_ref()

    client.native
    |> Nif.get_visitor_warehouse_audience(
      visitor_code,
      custom_data_index,
      warehouse_key,
      self(),
      ref
    )
    |> NativeResult.ref(ref)
  end

  defp await(ref) when is_reference(ref) do
    await_native_result(:ok, ref)
  end

  defp await_native_ok(status, ref, timeout \\ @native_callback_timeout)

  defp await_native_ok(:ok, ref, timeout) do
    case await_native_result(:ok, ref, timeout) do
      {:ok, _result} -> :ok
      error -> error
    end
  end

  defp await_native_result(status, ref, timeout \\ @native_callback_timeout)

  defp await_native_result(:ok, ref, timeout) do
    receive do
      {@native_reply_tag, ^ref, {:ok, result}} -> {:ok, result}
      {@native_reply_tag, ^ref, {:error, payload}} -> NativeResult.error(payload)
    after
      timeout -> {:error, %Error{code: "Internal", message: "Native operation timed out"}}
    end
  end
end