defmodule FileConfig do
@moduledoc "Public API"
@table FileConfig.Loader
@match_limit 500
@type table_name :: atom()
@opaque version :: {:vsn, term()}
@type reason :: atom() | binary()
# :ets.continuation()
@type continuation :: term()
@doc "Read value from table"
@spec read(table_name(), term()) :: {:ok, term()} | nil | {:error, reason()}
# @spec read(table_name(), term()) :: {:ok, term()} | :undefined
def read(table_name, key) do
case table_info(table_name) do
{:ok, %{handler: handler} = table_state} ->
handler.read(table_state, key)
err ->
err
end
end
@doc "Insert one or more records"
@spec insert(table_name(), {term(), term()} | [{term(), term()}]) :: :ok | {:error, reason()}
# @spec insert(table_name(), {atom(), term()} | [{atom(), term()}]) :: true
def insert(table_name, records) do
case table_info(table_name) do
{:ok, %{handler: handler} = table_state} ->
handler.insert_records(table_state, records)
err ->
err
end
end
# @deprecated "Use read_all/2 instead"
@spec all(table_name(), pos_integer()) :: list(term())
def all(table_name, match_limit \\ @match_limit) do
{:ok, value} = read_all(table_name, match_limit)
value
end
@doc "Return all records"
@spec read_all(table_name(), pos_integer()) :: {:ok, list()}
def read_all(table_name, match_limit \\ @match_limit) do
{:ok, tab} = table(table_name)
loop_all(tab, :_, match_limit)
end
@spec loop_all(:ets.tab(), :ets.match_pattern(), pos_integer()) :: {:ok, list()}
defp loop_all(tab, pat, limit) do
loop_all(:ets.match_object(tab, pat, limit), [])
end
@spec loop_all({list(), continuation()} | :"$end_of_table", list()) :: {:ok, list()}
defp loop_all({match, continuation}, acc) do
loop_all(:ets.match_object(continuation), [match | acc])
end
defp loop_all(:"$end_of_table", acc) do
{:ok, List.flatten(Enum.reverse(acc))}
end
@spec flush(table_name()) :: :ok | {:error, reason()}
# @spec flush(table_name()) :: true
def flush(table_name) do
case table_info(table_name) do
{:ok, %{handler: _handler} = table_state} ->
# TODO: This should call flush on handler
# handler.flush(table_state)
:ets.delete_all_objects(table_state.id)
:ok
err ->
err
end
end
# The version is the table id, which should be swapped on
# any update. This is a very scary thing to use, but it works
# as long as we use it as an opaque data type.
@spec version(table_name()) :: version()
def version(table_name), do: {:vsn, table(table_name)}
@spec version(table_name(), version()) :: :current | :old
def version(table_name, {:vsn, version}) do
if table(table_name) == version do
:current
else
:old
end
end
# Private
# @doc "Get table id from index"
@spec table(table_name()) :: {:ok, :ets.tab()} | {:error, :unknown_table}
defp table(table_name) do
case :ets.lookup(@table, table_name) do
[{^table_name, %{id: tab}}] ->
{:ok, tab}
[] ->
{:error, :unknown_table}
end
catch
:error, :badarg ->
{:error, :unknown_table}
end
@doc "Get all data from index"
@spec table_info(table_name()) :: {:ok, FileConfig.Loader.table_state()} | {:error, :unknown_table}
# @spec table_info(table_name()) :: FileConfig.Loader.table_state() | :undefined
def table_info(table_name) do
case :ets.lookup(@table, table_name) do
[{^table_name, value}] ->
{:ok, value}
[] ->
{:error, :unknown_table}
end
catch
:error, :badarg ->
{:error, :unknown_table}
end
end