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
      config NavEx.Adapters.ETS,
        identity_key: "nav_ex_identity", # name of the key in cookies where the user's identity is saved
        table_name: :navigation_history # name of the ETS table
  """

  @behaviour NavEx.Adapter
  import Plug.Conn

  alias NavEx.Adapters.ETS.RecordsStorage

  @key_length 128
  @cookies_key Application.compile_env(NavEx.Adapters.ETS, :identity_key) || "nav_ex_identity"

  @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)
    {:ok, _result} = RecordsStorage.insert(user_identity, request_path)

    {:ok, conn}
  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 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 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

  ###

  defp maybe_insert_identity(%Plug.Conn{} = conn) do
    case fetch_cookies(conn) do
      %{cookies: %{@cookies_key => user_identity}} ->
        {conn, user_identity}

      _no_identity ->
        user_identity = create_user_identity()
        conn = put_resp_cookie(conn, @cookies_key, user_identity)
        {conn, user_identity}
    end
  end

  defp get_identity(%Plug.Conn{} = conn) do
    case fetch_cookies(conn) do
      %{cookies: %{@cookies_key => user_identity}} ->
        {:ok, user_identity}

      _no_identity ->
        {:error, :not_found}
    end
  end

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