lib/ets/base.ex

defmodule ETS.Base do
  @moduledoc """
  Base implementation for table modules (e.g. `ETS.Set` and `ETS.Bag`). Should not be used directly.

  """
  use ETS.Utils

  @protection_types [:public, :protected, :private]
  @type option ::
          {:name, atom()}
          | {:protection, :private | :protected | :public}
          | {:heir, :none | {pid(), any()}}
          | {:keypos, non_neg_integer()}
          | {:write_concurrency, boolean()}
          | {:read_concurrency, boolean()}
          | {:compressed, boolean()}
  @type options :: [option]
  @type table_types :: :bag | :duplicate_bag | :ordered_set | :set
  @table_types [:bag, :duplicate_bag, :ordered_set, :set]

  @doc false
  @spec new_table(table_types(), keyword()) ::
          {:ok, {ETS.table_reference(), keyword()}} | {:error, any()}
  def new_table(type, opts) when type in @table_types and is_list(opts) do
    {opts, name} = take_opt(opts, :name, nil)

    if is_atom(name) do
      starting_opts =
        if is_nil(name) do
          [type]
        else
          [:named_table, type]
        end

      case parse_opts(starting_opts, opts) do
        {:ok, parsed_opts} ->
          catch_table_already_exists name do
            info =
              name
              |> :ets.new(parsed_opts)
              |> :ets.info()

            ref = info[:id]
            {:ok, {ref, info}}
          end

        {:error, reason} ->
          {:error, reason}
      end
    else
      {:error, {:invalid_option, {:name, name}}}
    end
  end

  @spec parse_opts(list(), options) :: {:ok, list()} | {:error, {:invalid_option, any()}}
  defp parse_opts(acc, [{:protection, protection} | tl]) when protection in @protection_types,
    do: parse_opts([protection | acc], tl)

  defp parse_opts(acc, [{:heir, {pid, heir_data}} | tl]) when is_pid(pid),
    do: parse_opts([{:heir, pid, heir_data} | acc], tl)

  defp parse_opts(acc, [{:heir, :none} | tl]), do: parse_opts([{:heir, :none} | acc], tl)

  defp parse_opts(acc, [{:keypos, keypos} | tl]) when is_integer(keypos) and keypos >= 0,
    do: parse_opts([{:keypos, keypos} | acc], tl)

  defp parse_opts(acc, [{:write_concurrency, wc} | tl]) when is_boolean(wc),
    do: parse_opts([{:write_concurrency, wc} | acc], tl)

  defp parse_opts(acc, [{:read_concurrency, rc} | tl]) when is_boolean(rc),
    do: parse_opts([{:read_concurrency, rc} | acc], tl)

  defp parse_opts(acc, [{:compressed, true} | tl]), do: parse_opts([:compressed | acc], tl)
  defp parse_opts(acc, [{:compressed, false} | tl]), do: parse_opts(acc, tl)

  defp parse_opts(acc, []), do: {:ok, acc}

  defp parse_opts(_, [bad_val | _]),
    do: {:error, {:invalid_option, bad_val}}

  @doc false
  @spec info(ETS.table_identifier()) :: {:ok, keyword()} | {:error, :table_not_found}
  def info(table) do
    catch_error do
      case :ets.info(table) do
        :undefined -> {:error, :table_not_found}
        x -> {:ok, x}
      end
    end
  end

  @doc false
  @spec insert(ETS.table_identifier(), tuple(), any()) :: {:ok, any()} | {:error, any()}
  def insert(table, record, return) do
    catch_error do
      catch_write_protected table do
        catch_record_too_small table, record do
          catch_table_not_found table do
            :ets.insert(table, record)
            {:ok, return}
          end
        end
      end
    end
  end

  @doc false
  @spec insert_new(ETS.table_identifier(), tuple(), any()) :: {:ok, any()} | {:error, any()}
  def insert_new(table, record, return) do
    catch_error do
      catch_write_protected table do
        catch_record_too_small table, record do
          catch_table_not_found table do
            :ets.insert_new(table, record)
            {:ok, return}
          end
        end
      end
    end
  end

  @doc false
  @spec insert_multi(ETS.table_identifier(), list(tuple()), any()) ::
          {:ok, any()} | {:error, any()}
  def insert_multi(table, records, return) do
    catch_error do
      catch_write_protected table do
        catch_records_too_small table, records do
          catch_bad_records records do
            catch_table_not_found table do
              :ets.insert(table, records)
              {:ok, return}
            end
          end
        end
      end
    end
  end

  @doc false
  @spec insert_multi_new(ETS.table_identifier(), list(tuple), any()) ::
          {:ok, any()} | {:error, any()}
  def insert_multi_new(table, records, return) do
    catch_error do
      catch_write_protected table do
        catch_records_too_small table, records do
          catch_bad_records records do
            catch_table_not_found table do
              :ets.insert_new(table, records)
              {:ok, return}
            end
          end
        end
      end
    end
  end

  @doc false
  @spec to_list(ETS.table_identifier()) :: {:ok, [tuple()]} | {:error, any()}
  def to_list(table) do
    catch_error do
      catch_table_not_found table do
        {:ok, :ets.tab2list(table)}
      end
    end
  end

  @doc false
  @spec lookup(ETS.table_identifier(), any()) :: {:ok, [tuple()]} | {:error, any()}
  def lookup(table, key) do
    catch_error do
      catch_read_protected table do
        catch_table_not_found table do
          vals = :ets.lookup(table, key)
          {:ok, vals}
        end
      end
    end
  end

  @doc false
  @spec lookup_element(ETS.table_identifier(), any(), non_neg_integer()) ::
          {:ok, any()} | {:error, any()}
  def lookup_element(table, key, pos) do
    catch_error do
      catch_position_out_of_bounds table, key, pos do
        catch_key_not_found table, key do
          catch_read_protected table do
            catch_table_not_found table do
              vals = :ets.lookup_element(table, key, pos)
              {:ok, vals}
            end
          end
        end
      end
    end
  end

  @doc false
  @spec match(ETS.table_identifier(), ETS.match_pattern()) :: {:ok, [tuple()]} | {:error, any()}
  def match(table, pattern) do
    catch_error do
      catch_read_protected table do
        catch_table_not_found table do
          matches = :ets.match(table, pattern)
          {:ok, matches}
        end
      end
    end
  end

  @doc false
  @spec match(ETS.table_identifier(), ETS.match_pattern(), non_neg_integer()) ::
          {:ok, {[tuple()], any()}} | {:error, any()}
  def match(table, pattern, limit) do
    catch_error do
      catch_read_protected table do
        catch_table_not_found table do
          case :ets.match(table, pattern, limit) do
            {x, :"$end_of_table"} -> {:ok, {x, :end_of_table}}
            {records, continuation} -> {:ok, {records, continuation}}
            :"$end_of_table" -> {:ok, {[], :end_of_table}}
          end
        end
      end
    end
  end

  @doc false
  @spec match(any()) :: {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()}
  def match(continuation) do
    catch_error do
      try do
        case :ets.match(continuation) do
          {x, :"$end_of_table"} -> {:ok, {x, :end_of_table}}
          {records, continuation} -> {:ok, {records, continuation}}
          :"$end_of_table" -> {:ok, {[], :end_of_table}}
        end
      rescue
        ArgumentError ->
          {:error, :invalid_continuation}
      end
    end
  end

  @doc false
  @spec match_delete(ETS.table_identifier(), ETS.match_pattern()) :: :ok | {:error, any()}
  def match_delete(table, pattern) do
    catch_error do
      catch_read_protected table do
        catch_table_not_found table do
          :ets.match_delete(table, pattern)
          :ok
        end
      end
    end
  end

  @doc false
  @spec match_object(ETS.table_identifier(), ETS.match_pattern()) ::
          {:ok, [tuple()]} | {:error, any()}
  def match_object(table, pattern) do
    catch_error do
      catch_read_protected table do
        catch_table_not_found table do
          matches = :ets.match_object(table, pattern)
          {:ok, matches}
        end
      end
    end
  end

  @doc false
  @spec match_object(ETS.table_identifier(), ETS.match_pattern(), non_neg_integer()) ::
          {:ok, {[tuple()], any()}} | {:error, any()}
  def match_object(table, pattern, limit) do
    catch_error do
      catch_read_protected table do
        catch_table_not_found table do
          case :ets.match_object(table, pattern, limit) do
            {x, :"$end_of_table"} -> {:ok, {x, :end_of_table}}
            {records, continuation} -> {:ok, {records, continuation}}
            :"$end_of_table" -> {:ok, {[], :end_of_table}}
          end
        end
      end
    end
  end

  @doc false
  @spec match_object(any()) :: {:ok, {[tuple()], any() | :end_of_table}} | {:error, any()}
  def match_object(continuation) do
    catch_error do
      try do
        case :ets.match_object(continuation) do
          {x, :"$end_of_table"} -> {:ok, {x, :end_of_table}}
          {records, continuation} -> {:ok, {records, continuation}}
          :"$end_of_table" -> {:ok, {[], :end_of_table}}
        end
      rescue
        ArgumentError ->
          {:error, :invalid_continuation}
      end
    end
  end

  @spec select(ETS.continuation()) ::
          {:ok, {[tuple()], ETS.continuation()} | ETS.end_of_table()} | {:error, any()}
  def select(continuation) do
    catch_error do
      catch_invalid_continuation continuation do
        matches = :ets.select(continuation)
        {:ok, matches}
      end
    end
  end

  @doc false
  @spec select(ETS.table_identifier(), ETS.match_spec()) :: {:ok, [tuple()]} | {:error, any()}
  def select(table, spec) when is_list(spec) do
    catch_error do
      catch_read_protected table do
        catch_invalid_select_spec spec do
          catch_table_not_found table do
            matches = :ets.select(table, spec)
            {:ok, matches}
          end
        end
      end
    end
  end

  @doc false
  @spec select(ETS.table_identifier(), ETS.match_spec(), limit :: integer) ::
          {:ok, {[tuple()], ETS.continuation()} | ETS.end_of_table()} | {:error, any()}
  def select(table, spec, limit) when is_list(spec) do
    catch_error do
      catch_read_protected table do
        catch_invalid_select_spec spec do
          catch_table_not_found table do
            matches = :ets.select(table, spec, limit)
            {:ok, matches}
          end
        end
      end
    end
  end

  @doc false
  @spec select_delete(ETS.table_identifier(), ETS.match_spec()) ::
          {:ok, non_neg_integer()} | {:error, any()}
  def select_delete(table, spec) when is_list(spec) do
    catch_error do
      catch_read_protected table do
        catch_invalid_select_spec spec do
          catch_table_not_found table do
            count = :ets.select_delete(table, spec)
            {:ok, count}
          end
        end
      end
    end
  end

  @doc false
  @spec has_key(ETS.table_identifier(), any()) :: {:ok, boolean()} | {:error, any()}
  def has_key(table, key) do
    catch_error do
      catch_read_protected table do
        catch_table_not_found table do
          {:ok, :ets.member(table, key)}
        end
      end
    end
  end

  @doc false
  @spec first(ETS.table_identifier()) :: {:ok, any()} | {:error, any()}
  def first(table) do
    catch_error do
      catch_read_protected table do
        catch_table_not_found table do
          case :ets.first(table) do
            :"$end_of_table" -> {:error, :empty_table}
            x -> {:ok, x}
          end
        end
      end
    end
  end

  @doc false
  @spec last(ETS.table_identifier()) :: {:ok, any()} | {:error, any()}
  def last(table) do
    catch_error do
      catch_read_protected table do
        catch_table_not_found table do
          case :ets.last(table) do
            :"$end_of_table" -> {:error, :empty_table}
            x -> {:ok, x}
          end
        end
      end
    end
  end

  @doc false
  @spec next(ETS.table_identifier(), any()) :: {:ok, any()} | {:error, any()}
  def next(table, key) do
    catch_error do
      catch_read_protected table do
        catch_table_not_found table do
          case :ets.next(table, key) do
            :"$end_of_table" -> {:error, :end_of_table}
            x -> {:ok, x}
          end
        end
      end
    end
  end

  @doc false
  @spec previous(ETS.table_identifier(), any()) :: {:ok, any()} | {:error, any()}
  def previous(table, key) do
    catch_error do
      catch_read_protected table do
        catch_table_not_found table do
          case :ets.prev(table, key) do
            :"$end_of_table" -> {:error, :start_of_table}
            x -> {:ok, x}
          end
        end
      end
    end
  end

  @doc false
  @spec delete(ETS.table_identifier(), any()) :: {:ok, any()} | {:error, any()}
  def delete(table, return) do
    catch_error do
      catch_write_protected table do
        catch_table_not_found table do
          :ets.delete(table)
          {:ok, return}
        end
      end
    end
  end

  @doc false
  @spec delete_records(ETS.table_identifier(), any(), any()) :: {:ok, any()} | {:error, any()}
  def delete_records(table, key, return) do
    catch_error do
      catch_write_protected table do
        catch_table_not_found table do
          :ets.delete(table, key)
          {:ok, return}
        end
      end
    end
  end

  @doc false
  @spec delete_all_records(ETS.table_identifier(), any()) :: {:ok, any()} | {:error, any()}
  def delete_all_records(table, return) do
    catch_error do
      catch_write_protected table do
        catch_table_not_found table do
          :ets.delete_all_objects(table)
          {:ok, return}
        end
      end
    end
  end

  @doc false
  @spec wrap_existing(ETS.table_identifier(), [table_types]) ::
          {:ok, {ETS.table_reference(), keyword()}} | {:error, any()}
  def wrap_existing(table, valid_types) do
    catch_error do
      catch_table_not_found table do
        case :ets.info(table) do
          :undefined ->
            {:error, :table_not_found}

          info ->
            if info[:type] in valid_types do
              {:ok, {info[:id], info}}
            else
              {:error, :invalid_type}
            end
        end
      end
    end
  end

  @doc false
  @spec update_element(ETS.table_identifier(), any(), tuple() | [tuple()]) ::
          boolean() | {:error, any()}
  def update_element(table, key, element_spec) do
    catch_error do
      catch_key_update table, element_spec do
        catch_positions_out_of_bounds table, key, element_spec do
          catch_write_protected table do
            catch_table_not_found table do
              :ets.update_element(table, key, element_spec)
            end
          end
        end
      end
    end
  end

  @spec give_away(ETS.table_identifier(), pid(), any(), any()) :: {:ok, any()} | {:error, any()}
  def give_away(table, pid, gift, return) do
    catch_error do
      catch_sender_not_table_owner table do
        catch_recipient_not_alive pid do
          catch_recipient_already_owns_table table, pid do
            catch_table_not_found table do
              :ets.give_away(table, pid, gift)
              {:ok, return}
            end
          end
        end
      end
    end
  end

  @doc false
  @spec accept(integer() | :infinity) ::
          {:ok, ETS.table_identifier(), pid(), any()} | {:error, :timeout}
  def accept(timeout) do
    receive do
      {:"ETS-TRANSFER", table, from, gift} ->
        {:ok, table, from, gift}
    after
      timeout ->
        {:error, :timeout}
    end
  end

  defmacro accept(id, table, from, state, do: contents) do
    quote do
      def handle_info(
            {:"ETS-TRANSFER", unquote(table), unquote(from), unquote(id)},
            unquote(state)
          ) do
        var!(unquote(table)) = unquote(table)
        unquote(contents)
      end
    end
  end
end