lib/cache.ex

defmodule Cache do
  @moduledoc "#{File.read!("./README.md")}"

  use Supervisor

  @callback child_spec({
              cache_name :: atom,
              cache_opts :: Keyword.t()
            }) :: Supervisor.child_spec() | :supervisor.child_spec()

  @callback opts_definition() :: Keyword.t()

  @callback start_link(cache_opts :: Keyword.t()) :: {:ok, pid()} | {:error, {:already_started, pid()} | {:shutdown, term()} | term()} | :ignore

  @callback put(cache_name :: atom, key :: atom | String.t(), ttl :: pos_integer, value :: any) ::
              :ok | ErrorMessage.t()
  @callback put(
              cache_name :: atom,
              key :: atom | String.t(),
              ttl :: pos_integer,
              value :: any,
              Keyword.t()
            ) :: :ok | ErrorMessage.t()

  @callback get(cache_name :: atom, key :: atom | String.t()) :: ErrorMessage.t_res(any)
  @callback get(cache_name :: atom, key :: atom | String.t(), Keyword.t()) ::
              ErrorMessage.t_res(any)

  @callback delete(cache_name :: atom, key :: atom | String.t(), opts :: Keyword.t()) ::
              :ok | ErrorMessage.t()
  @callback delete(cache_name :: atom, key :: atom | String.t()) :: :ok | ErrorMessage.t()

  defmacro __using__(opts) do
    quote do
      opts = unquote(opts)

      @cache_opts opts
      @cache_name opts[:name]
      @cache_adapter if opts[:sandbox?], do: Cache.Sandbox, else: opts[:adapter]

      if !opts[:adapter] do
        raise "Must supply a cache adapter for #{__MODULE__}"
      end

      if !@cache_name do
        raise "Must supply a cache name for #{__MODULE__}"
      end

      pre_check_runtime_options = fn
        {_, _, _} = mfa ->
          mfa

        {_, _} = app_config ->
          app_config

        fun when is_function(fun, 0) ->
          fun

        app_name when is_atom(app_name) and not is_nil(app_name) ->
          app_name

        val ->
          raise ArgumentError, """
          Bad option in adapter module #{inspect(__MODULE__)}!

          Expected one of the following:

            * `{module, function, args}` - Module, function, args
            * `{application_name, key}` - Application name. This is called as `Application.fetch_env!(application_name, key)`.
            * `application_name` - Application name as an atom. This is called as `Application.fetch_env!(application_name, #{inspect(__MODULE__)})`.
            * `function` - Zero arity callback function. For eg. `&YourModule.options/0`
            * `[key: value_type]` - Keyword list of options.

          Got: #{inspect(val)}
          """
      end

      check_adapter_opts = fn
        adapter_opts when is_list(adapter_opts) ->
          NimbleOptions.validate!(adapter_opts, @cache_adapter.opts_definition())

        adapter_opts ->
          pre_check_runtime_options.(adapter_opts)

      end

      adapter_opts = if opts[:sandbox?], do: [], else: check_adapter_opts.(opts[:opts])

      @adapter_opts adapter_opts
      @compression_level if is_list(@adapter_opts), do: @adapter_opts[:compression_level]

      if macro_exported?(unquote(opts[:adapter]), :__using__, 1) do
        use unquote(opts[:adapter])
      end

      def cache_name, do: @cache_name
      def cache_adapter, do: @cache_adapter

      def adapter_options, do: adapter_options!(@adapter_opts)

      defp adapter_options!({module, fun, args}), do: apply(module, fun, args)
      defp adapter_options!({app, key}), do: Application.fetch_env!(app, key)
      defp adapter_options!(app_name) when is_atom(app_name), do: Application.fetch_env!(app_name, __MODULE__)
      defp adapter_options!(fun) when is_function(fun, 0), do: fun.()
      defp adapter_options!(options), do: options

      def child_spec(_) do
        @cache_adapter.child_spec({@cache_name, adapter_options()})
      end

      def put(key, ttl \\ nil, value) do
        value = Cache.TermEncoder.encode(value, @compression_level)
        key = maybe_sandbox_key(key)

        @cache_adapter.put(@cache_name, key, ttl, value, adapter_options())
      end

      def get(key) do
        key = maybe_sandbox_key(key)

        with {:ok, value} when not is_nil(value) <-
               @cache_adapter.get(@cache_name, key, adapter_options()) do
          {:ok, Cache.TermEncoder.decode(value)}
        end
      end

      def delete(key) do
        key = maybe_sandbox_key(key)

        @cache_adapter.delete(@cache_name, key, adapter_options())
      end

      if @cache_opts[:sandbox?] do
        defp maybe_sandbox_key(key) do
          sandbox_id = Cache.SandboxRegistry.find!(__MODULE__)

          "#{sandbox_id}:#{key}"
        end
      else
        defp maybe_sandbox_key(key) do
          key
        end
      end
    end
  end

  def start_link(cache_children, opts \\ []) do
    Supervisor.start_link(Cache, cache_children, opts)
  end

  def init(cache_children) do
    Supervisor.init(cache_children, strategy: :one_for_one)
  end
end