defmodule CacheMoney do
@moduledoc """
Handles caching values under different cache names, can expire keys
"""
use GenServer
@default_timeout 5000
@typedoc """
The name of the cache, used for namespacing multiple caches on the same adapter.
Can be either a binary or an atom, but will always be converted to a binary.
"""
@type cache_name :: binary | atom
@typedoc """
The key a value will be set under. Can be either a binary or an atom, but will
always be converted to a binary.
"""
@type key :: binary | atom
@typedoc """
The value to be saved in the cache. Can be any value going _in_ to the cache,
but depending on the adapter used, may not be the same value going out. For
example, `CacheMoney.Adapters.ETS` can save any elixir term, including `pid`s.
`CacheMoney.Adapters.Redis`, however, can only save items as strings.
"""
@type value :: term
@typedoc """
Currently the only option available is an optional `timeout` that gets passed
along with `GenServer.call`
"""
@type options :: [timeout: integer]
@type lazy_function :: (() -> {:ok, value} | {:ok, value, integer} | {:error, value} | value)
@type server :: GenServer.server()
@doc """
Starts a `CacheMoney` process linked to the current process.
## Arguments
* cache - the name of the cache. Multiple caches using the same adapter will
all be in the same spot, but will be namespaced by the given cache name.
* conifg - contains various configuration options for the cache, depending on
the adapter. `:adapter` is required to be set, and must be set to a module that
implements `CacheMoney.Adapter`, such as `CacheMoney.Adapters.Redis` or
`CacheMoney.Adapters.ETS`. Different adapters will also specify other required
optionso be passed to them through the `config` argument
* opts - see `GenServer.start_link/3`. Options are passed straight through to the
underlying `GenServer`
"""
@spec start_link(cache_name, map(), Keyword.t()) :: GenServer.on_start()
def start_link(cache, config = %{}, opts \\ []) do
config =
config
|> Map.put_new(:cache, cache)
|> config.adapter.start_link()
opts = Keyword.put_new(opts, :name, cache)
GenServer.start_link(__MODULE__, config, opts)
end
@doc """
Gets the value out of the cache using the `key`.
If the value does not exist in the cache `nil` will be returned.
"""
@spec get(server, key, options()) :: {:ok, value} | {:error, term}
def get(server, key, opts \\ []),
do: GenServer.call(server, {:get, key}, opts[:timeout] || @default_timeout)
@doc """
Gets the value out of the cache using the `key`. Lazily fetches the data, inserts
it into the cache, and returns it if it does not exist. Optional `expiry` is in
seconds.
"""
@spec get_lazy(server, key, lazy_function(), integer | nil, options()) ::
{:ok, value} | {:error, any}
def get_lazy(server, key, fun, expiry \\ nil, opts \\ []),
do: GenServer.call(server, {:get_lazy, key, fun, expiry}, opts[:timeout] || @default_timeout)
@doc """
Sets `key` in the cache to `value`
"""
@spec set(server, key, value) :: {:ok, value} | {:error, any}
def set(server, key, value), do: GenServer.call(server, {:set, key, value})
@doc """
Sets `key` in the cache to `value`
"""
@spec set(server, key, value, options()) :: {:ok, value} | {:error, any}
def set(server, key, value, opts) when is_list(opts),
do: GenServer.call(server, {:set, key, value}, opts[:timeout] || @default_timeout)
@doc """
Sets `key` in the cache to `value`, which expires after `expiry` seconds
"""
@spec set(server, key, value, integer, options()) :: {:ok, value} | {:error, any}
def set(server, key, value, expiry, opts \\ []),
do: GenServer.call(server, {:set, key, value, expiry}, opts[:timeout] || @default_timeout)
@doc """
Deletes the `key` from the cache
"""
@spec delete(server, key, options()) :: {:ok, value} | {:error, term}
def delete(server, key, opts \\ []),
do: GenServer.call(server, {:delete, key}, opts[:timeout] || @default_timeout)
@impl true
def init(args) do
{:ok, args}
end
@impl true
def handle_call({:get, key}, from, config) do
key = get_key(config.cache, key)
{:reply, config.adapter.get(with_caller(config, from), key), config}
end
def handle_call({:get_lazy, key, fun, expiry}, from, config) do
key = get_key(config.cache, key)
case config.adapter.get(with_caller(config, from), key) do
{:ok, nil} ->
value = get_and_save_lazy_value(key, fun.(), expiry, with_caller(config, from))
{:reply, value, config}
value ->
{:reply, value, config}
end
end
def handle_call({:set, key, value}, from, config) do
{:reply, config.adapter.set(with_caller(config, from), get_key(config.cache, key), value),
config}
end
def handle_call({:set, key, value, expiry}, from, config) do
{:reply,
config.adapter.set(
with_caller(config, from),
get_key(config.cache, key),
value,
expiry
), config}
end
def handle_call({:delete, key}, from, config) do
{:reply, config.adapter.delete(with_caller(config, from), get_key(config.cache, key)), config}
end
defp get_and_save_lazy_value(key, {:ok, value}, nil, config) do
config.adapter.set(config, key, value)
{:ok, value}
end
defp get_and_save_lazy_value(key, {:ok, value, expiry}, nil, config) do
config.adapter.set(config, key, value, expiry)
{:ok, value}
end
defp get_and_save_lazy_value(key, {:ok, value}, expiry, config) do
config.adapter.set(config, key, value, expiry)
{:ok, value}
end
defp get_and_save_lazy_value(_key, {:error, error}, _, _config), do: {:error, error}
defp get_and_save_lazy_value(key, value, nil, config) do
config.adapter.set(config, key, value)
{:ok, value}
end
defp get_and_save_lazy_value(key, value, expiry, config) do
config.adapter.set(config, key, value, expiry)
{:ok, value}
end
defp get_key(cache, key) do
"#{cache}-#{key}"
end
defp with_caller(config, {caller, _}), do: Map.put(config, :caller, caller)
end