defmodule Stash do @moduledoc """ This module provides a convenient interface around ETS/DTS without taking large performance hits. Designed for being thrown into a project for basic memory-based storage, perhaps with some form of persistence required. """ @doc """ Removes all items from all namespaces. ## Examples iex> Stash.put(:namespace1, "key1", "value1") iex> Stash.put(:namespace2, "key2", "value2") iex> Stash.put(:namespace3, "key3", "value3") iex> Stash.clear() true iex> Stash.size(:namespace1) 0 iex> Stash.size(:namespace2) 0 iex> Stash.size(:namespace3) 0 """ @spec clear() :: true def clear(), do: :ets.delete_all_objects(:"$stash") @doc """ Removes a value from the namespace. ## Examples iex> Stash.put(:my_namespace, "key", "value") iex> Stash.get(:my_namespace, "key") "value" iex> Stash.delete(:my_namespace, "key") true iex> Stash.get(:my_namespace, "key") nil """ @spec delete(atom(), any()) :: true def delete(namespace, key), do: :ets.delete(:"$stash", {namespace, key}) @doc """ Removes all items from a namespace. ## Examples iex> Stash.put(:namespace, "key1", "value1") iex> Stash.put(:namespace, "key2", "value2") iex> Stash.put(:namespace, "key3", "value3") iex> Stash.drop(:namespace) true iex> Stash.size(:namespace) 0 """ @spec drop(atom()) :: true def drop(namespace), do: :ets.select_delete(:"$stash", [ {{{namespace, :_}, :_}, [], [true]} ]) @doc """ Checks whether the namespace is empty. ## Examples iex> Stash.put(:my_namespace, "key1", "value1") iex> Stash.put(:my_namespace, "key2", "value2") iex> Stash.put(:my_namespace, "key3", "value3") iex> Stash.empty?(:my_namespace) false iex> Stash.clear(:my_namespace) true iex> Stash.empty?(:my_namespace) true """ @spec empty?(atom()) :: boolean() def empty?(namespace), do: size(namespace) == 0 @doc """ Determines whether a given key exists inside the namespace. ## Examples iex> Stash.put(:my_namespace, "key", "value") iex> Stash.exists?(:my_namespace, "key") true iex> Stash.exists?(:my_namespace, "missing_key") false """ @spec exists?(atom, any) :: true | false def exists?(namespace, key), do: get(namespace, key, :"$exists") != :"$exists" @doc """ Retrieves a value from the namespace. ## Examples iex> Stash.put(:my_namespace, "key", "value") iex> Stash.get(:my_namespace, "key") "value" iex> Stash.get(:my_namespace, "missing_key") nil iex> Stash.get(:my_namespace, "missing_key, "default") "default """ @spec get(atom(), any(), any()) :: any() def get(namespace, key, default \\ nil) do case :ets.lookup(:"$stash", {namespace, key}) do [{{^namespace, ^key}, value}] -> value _unrecognised -> default end end @doc """ Increments a key directly in the namespace by `count`. If the key does not exist it is set to `initial` before **then** being incremented. ## Examples iex> Stash.put(:my_namespace, "key", 1) iex> Stash.increment(:my_namespace, "key") 2 iex> Stash.increment(:my_namespace, "key", 2) 4 iex> Stash.increment(:my_namespace, "missing_key", 1) 1 iex> Stash.increment(:my_namespace, "a_missing_key", 1, 5) 6 """ @spec increment(atom(), any(), number(), number()) :: number() def increment(namespace, key, count \\ 1, initial \\ 0) when is_number(count) and is_number(initial), do: :ets.update_counter( :"$stash", {namespace, key}, {2, count}, {key, initial} ) @doc """ Returns information about the backing ETS table. ## Examples iex> Stash.info() [read_concurrency: true, write_concurrency: true, compressed: false, memory: 1361, owner: #PID<0.126.0>, heir: :none, name: :my_namespace, size: 2, node: :nonode@nohost, named_table: true, type: :set, keypos: 1, protection: :public] """ @spec info() :: [{atom(), any()}] def info(), do: :ets.info(:"$stash") @doc """ Retrieves all keys from the namespace, and returns them as an (unordered) list. ## Examples iex> Stash.put(:my_namespace, "key1", "value1") iex> Stash.put(:my_namespace, "key2", "value2") iex> Stash.put(:my_namespace, "key3", "value3") iex> Stash.keys(:my_namespace) [ "key2", "key1", "key3" ] iex> Stash.keys(:empty_namespace) [] """ @spec keys(atom()) :: [any()] def keys(namespace), do: :ets.foldr( fn {{^namespace, key}, _value}, keys -> [key | keys] _other, keys -> keys end, [], :"$stash" ) @doc """ Loads a namespace into memory from DTS storage. ## Examples iex> Stash.load("/tmp/temporary.dat") :ok """ @spec load(binary()) :: atom() def load(path) when is_binary(path) do args = gen_dts_args() path = :binary.bin_to_list(path) with {:ok, ^path} <- :dets.open_file(path, args) do :dets.to_ets(path, :"$stash") :dets.close(path) end end @doc """ Persists a namespace onto disk to allow reload after the process dies. ## Examples iex> Stash.persist(:my_namespace, "/tmp/temporary.dat") :ok """ @spec persist(binary()) :: atom() def persist(path) when is_binary(path) do args = gen_dts_args() path = :binary.bin_to_list(path) with {:ok, ^path} <- :dets.open_file(path, args) do :dets.from_ets(path, :"$stash") :dets.close(path) end end @doc """ Places a value in the namespace against a given key. ## Examples iex> Stash.put(:my_namespace, "key", "value") true """ @spec put(atom(), any(), any()) :: true def put(namespace, key, value), do: :ets.insert(:"$stash", {{namespace, key}, value}) @doc """ Removes a key from the namespace, whilst also returning the last known value. ## Examples iex> Stash.put(:my_namespace, "key", "value") iex> Stash.remove(:my_namespace, "key") "value" iex> Stash.get(:my_namespace, "key") nil iex> Stash.remove(:my_namespace, "missing_key") nil iex> Stash.remove(:my_namespace, "missing_key, "default") "default" """ @spec remove(atom(), any(), any()) :: any() def remove(namespace, key, default \\ nil) do case :ets.take(:"$stash", {namespace, key}) do [{{^namespace, ^key}, value}] -> value _unrecognised -> default end end @doc """ Determines the size of the namespace. ## Examples iex> Stash.put(:my_namespace, "key1", "value1") iex> Stash.put(:my_namespace, "key2", "value2") iex> Stash.put(:my_namespace, "key3", "value3") iex> Stash.size(:my_namespace) 3 """ @spec size(atom) :: number def size(namespace), do: :ets.select_count(:"$stash", [ {{{namespace, :_}, :_}, [], [true]} ]) # Generates the arguments for a DTS table. defp gen_dts_args(), do: [keypos: info()[:keypos], type: info()[:type]] end