lib/retry/annotation.ex

defmodule Retry.Annotation do
  @moduledoc """
  A @retry annotation that will retry the function according to the retry settings.

  Examples

      use Retry.Annotation

      @retry with: constant_backoff(100) |> Stream.take(10)
      def some_func(arg) do
        # ...
      end

  """

  defmacro __using__(_opts) do
    quote do
      import Retry.DelayStreams
      require Retry
      require Logger

      Module.register_attribute(__MODULE__, :retry_funs, accumulate: true)

      @on_definition {Retry.Annotation, :on_definition}
      @before_compile {Retry.Annotation, :before_compile}
    end
  end

  def on_definition(env, kind, name, args, guards, _body) do
    retry_opts = Module.get_attribute(env.module, :retry) || :no_retry

    unless retry_opts == :no_retry do
      Module.put_attribute(env.module, :retry_funs, %{
        kind: kind,
        name: name,
        args: Enum.map(args, &de_underscore_name/1),
        guards: guards,
        retry_opts: Keyword.update(retry_opts, :with, [], &Enum.to_list/1)
      })

      Module.delete_attribute(env.module, :retry)
    end
  end

  defmacro before_compile(env) do
    retry_funs = Module.get_attribute(env.module, :retry_funs, [])
    Module.delete_attribute(env.module, :retry_funs)

    override_list =
      retry_funs
      |> Enum.map(&gen_override_list/1)
      |> List.flatten()

    overrides =
      quote location: :keep do
        defoverridable unquote(override_list)
      end

    functions =
      Enum.map(retry_funs, fn fun ->
        body = gen_body(fun)
        gen_function(fun, body)
      end)

    [overrides | functions]
  end

  defp gen_override_list(%{name: name, args: args}) do
    no_default_args_length =
      Enum.reduce(args, 0, fn
        {:\\, _, _}, acc -> acc
        _, acc -> acc + 1
      end)

    Enum.map(no_default_args_length..length(args), fn i -> {name, i} end)
  end

  defp gen_function(%{kind: :def, guards: [], name: name, args: args}, body) do
    quote location: :keep do
      def unquote(name)(unquote_splicing(args)) do
        unquote(body)
      end
    end
  end

  defp gen_function(%{kind: :def, guards: guards, name: name, args: args}, body) do
    quote location: :keep do
      def unquote(name)(unquote_splicing(args)) when unquote_splicing(guards) do
        unquote(body)
      end
    end
  end

  defp gen_function(%{kind: :defp, guards: [], name: name, args: args}, body) do
    quote location: :keep do
      defp unquote(name)(unquote_splicing(args)) do
        unquote(body)
      end
    end
  end

  defp gen_function(%{kind: :defp, guards: guards, name: name, args: args}, body) do
    quote location: :keep do
      defp unquote(name)(unquote_splicing(args)) when unquote_splicing(guards) do
        unquote(body)
      end
    end
  end

  defp gen_body(fun) do
    args =
      Enum.map(fun.args, fn
        {:\\, _, [arg_name | _]} -> arg_name
        arg -> arg
      end)

    quote location: :keep do
      Retry.retry unquote(fun.retry_opts) do
        super(unquote_splicing(args))
      after
        result -> result
      else
        error -> error
      end
    end
  end

  defp de_underscore_name({:\\, context, [{name, name_context, name_args} | t]} = arg) do
    case to_string(name) do
      "_" <> real_name ->
        {:\\, context, [{String.to_atom(real_name), name_context, name_args} | t]}

      _ ->
        arg
    end
  end

  defp de_underscore_name({name, context, args} = arg) do
    case to_string(name) do
      "_" <> real_name -> {String.to_atom(real_name), context, args}
      _ -> arg
    end
  end
end