lib/adapters/ets/ets.ex

defmodule NavEx.Adapters.ETS do
  @moduledoc """
    NavEx.Adapters.ETS is adapter for keeping user's navigation history
    utilizing ETS. It uses NavEx.Adapters.ETS.RecordsStorage as the
    module to interact with the ETS table.

     ## Adapter config
     adapter_config: %{
        identity_key: "nav_ex_identity", # name of the key in session where the user's identity is saved
        table_name: :navigation_history # name of the ETS table
     }

     ## This adapter supports both Plug.Conn and Phoenix.LiveView.Socket.
     For LiveView sockets you have to make sure to put the same user's identity
     that was stored in the session into the socket assigns under the identity key (as atom!).
  """

  @behaviour NavEx.Adapter
  import Plug.Conn

  alias NavEx.Adapters.ETS.RecordsStorage

  @key_length 128
  @session_key Application.compile_env(:nav_ex, :adapter_config)[:identity_key] ||
                 "nav_ex_identity"

  @excluded_paths Application.compile_env(:nav_ex, :excluded_paths) || ["/exclude"]

  @impl NavEx.Adapter
  def children, do: [NavEx.Adapters.ETS.RecordsStorage]

  @impl NavEx.Adapter
  def insert(%Plug.Conn{request_path: request_path} = conn) do
    {conn, user_identity} = maybe_insert_identity(conn)

    if Enum.any?(@excluded_paths, &String.starts_with?(request_path, &1)) do
      {:ok, conn}
    else
      RecordsStorage.insert(user_identity, request_path)
      {:ok, conn}
    end
  end

  @impl NavEx.Adapter
  def insert(%Phoenix.LiveView.Socket{assigns: assigns} = socket, path) do
    key = String.to_atom(@session_key)
    user_identity = Map.get(assigns, key)

    if is_nil(user_identity) do
      raise ArgumentError,
            "NavEx.Adapters.ETS requires user identity to be set in socket assigns under the key ':#{@session_key}'."
    end

    if Enum.any?(@excluded_paths, &String.starts_with?(path, &1)) do
      {:ok, socket}
    else
      RecordsStorage.insert(user_identity, path)
      {:ok, socket}
    end
  end

  @impl NavEx.Adapter
  def list(%Plug.Conn{} = conn) do
    with {:ok, user_identity} <- get_identity(conn),
         {:ok, [{_user_identity, list}]} <- RecordsStorage.list(user_identity) do
      {:ok, list}
    end
  end

  @impl NavEx.Adapter
  def list(%Phoenix.LiveView.Socket{assigns: assigns} = _socket) do
    key = String.to_atom(@session_key)
    user_identity = Map.get(assigns, key)

    if is_nil(user_identity) do
      {:error, :not_found}
    else
      case RecordsStorage.list(user_identity) do
        {:ok, [{_user_identity, list}]} -> {:ok, list}
        error -> error
      end
    end
  end

  @impl NavEx.Adapter
  def last_path(%Plug.Conn{} = conn) do
    conn
    |> get_identity()
    |> case do
      {:ok, user_identity} ->
        RecordsStorage.last_path(user_identity)

      error ->
        error
    end
  end

  @impl NavEx.Adapter
  def last_path(%Phoenix.LiveView.Socket{assigns: assigns} = _socket) do
    key = String.to_atom(@session_key)
    user_identity = Map.get(assigns, key)

    case user_identity do
      nil ->
        {:error, :not_found}

      _ ->
        RecordsStorage.last_path(user_identity)
    end
  end

  @impl NavEx.Adapter
  def path_at(%Plug.Conn{} = conn, n) do
    conn
    |> get_identity()
    |> case do
      {:ok, user_identity} ->
        RecordsStorage.path_at(user_identity, n)

      error ->
        error
    end
  end

  @impl NavEx.Adapter
  def path_at(%Phoenix.LiveView.Socket{assigns: assigns}, n) do
    key = String.to_atom(@session_key)
    user_identity = Map.get(assigns, key)

    case user_identity do
      nil ->
        {:error, :not_found}

      _ ->
        RecordsStorage.path_at(user_identity, n)
    end
  end

  ###

  defp maybe_insert_identity(%Plug.Conn{} = conn) do
    case get_session(conn, @session_key) do
      nil ->
        user_identity = create_user_identity()
        conn = put_session(conn, @session_key, user_identity)
        {conn, user_identity}

      user_identity ->
        {conn, user_identity}
    end
  end

  defp get_identity(%Plug.Conn{} = conn) do
    case get_session(conn, @session_key) do
      nil ->
        {:error, :not_found}

      user_identity ->
        {:ok, user_identity}
    end
  end

  defp create_user_identity do
    :crypto.strong_rand_bytes(@key_length) |> Base.url_encode64() |> binary_part(0, @key_length)
  end
end