Extending dependency macro in my own macro?

I have a module that uses Tesla, and I want to enable usage of it like this

defmodule MyApp.SomeApi do
  
  use MyApp.Api.Client

  url "/some_api"

  def get_something() do
    get("/something") # <-- This should "default" to the Tesla get but with the url prepended like: get("/some_api/something")
  end
end

This is my code so far, help :frowning:

defmodule MyApp.Api.Client do
  defmacro __using__(_opts) do
    quote do
      use Tesla, docs: false
      plug(Tesla.Middleware.BaseUrl, get_url())
      plug(Tesla.Middleware.JSON)
      defp get_url(), do: Application.get_env(:my_app, :api_url)

      Module.register_attribute(__MODULE__, :api_url, [])
      import MyApp.Api.Client, only: [url: 1, get: 2]
      @before_compile MyApp.Api.Client
    end
  end

  defmacro url(path) do
    quote do
      @url unquote(path)
    end
  end

  defmacro get(url, params \\ []) do
    quote do
      get(@url <> unquote(url), unquote(params))
    end
  end

  defmacro __before_compile__(env) do
    quote do
      if Module.get_attribute(__MODULE__, :url) == nil do
        message =
          "The `url/1` must be defined in #{inspect(unquote(env.module))}. See docs for more"

        raise ArgumentError, message
      end
    end
  end
end

The BaseUrl plug will already do most of this, itโ€™s only a matter of giving it the right argument.

The options passed to Tesla.Builder.plug/2 are ultimately compiled into the generated __middlewares__ function, and so get evaluated at runtime (despite looking like Plug.Builder options that get evaluated at compile-time). That means you can keep this simpler (code has not been tested, beware):

defmodule MyApp.Api.Client do
  defmacro __using__(_opts) do
    quote do
      use Tesla, docs: false
      plug(Tesla.Middleware.BaseUrl, get_url())
      plug(Tesla.Middleware.JSON)
      defp get_url(), do: Application.get_env(:my_app, :api_url) <> api_url()

      import MyApp.Api.Client, only: [url: 1]
    end
  end

  defmacro url(path) do
    quote do
      def api_url, do: unquote(path)
    end
  end
end

The before_compile check is no longer needed, as failing to call url will result in the compiler complaining about a missing api_url/0.

One alternative worth considering: what about passing the extra URL component as an option to use MyApp.Api.Client?

2 Likes

That worked!
I was asking this because Iโ€™m working on Qdrant library for Elixir :slight_smile:

Man if you ever come to Serbia :serbia: you have a beer :beers: from me :hugs: :heart:

1 Like