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