I am writing a library that is essentially an HTTP API client, and I’m using Req as my default HTTP adapter. However, I don’t want to enforce Req as the only possible adapter, so I want to make the library’s user to be able to use their own custom HTTP adapter. Hence, I make Req an optional dependency, so it isn’t downloaded and compiled if it’s not used.
The module that implements the behaviour and essentially handles the use of adapter looks like this:
defmodule MyApp.HTTPClient do
@http_client Application.compile_env(:my_app, :http_client, MyApp.HTTPClient.Req)
@callback request(term()) :: term() # subject to change, obviously, as I finalize the adapter
def request(arg), do: @http_client.request(arg)
end
But I am having some trouble when it comes to implementing the default Req adapter and also polishing the overall custom adapter logic.
If the app is configured to use Req, but Req isn’t available, I feel like I need to raise an error at compile time, as opposed to during runtime. And also I think I should not compile the module and clutter the namespace if Req isn’t used. So I came up with something like this:
if !Application.compile_env(:my_app, :http_client) do
Code.ensure_compiled!(Req)
defmodule MyApp.HTTPClient.Req do
@behaviour MyApp.HTTPClient
# module logic here
end
end
However, I’m not sure if this is good practice to have such checks outside of module definitions. Besides, I’m afraid (maybe unreasonably) that this might break compilation order, optimizations and maybe something else. And, to be honest, it simply looks ugly and unconventionally.
I have also considered the following solution:
defmodule MyApp.HTTPClient.Req do
@behaviour MyApp.HTTPClient
@req Code.ensure_compiled?(Req)
if @req do
@impl true
def request(arg), do: Req.new ...
else
@impl true
def request(_), do: raise "Req is not available..."
end
end
But this obviously only works during runtime and possibly warns the user when it’s already too late. The error may even go unnoticed. And when my behaviour grows, I will have to implement that error for every single function, which is not optimal as well. And this might break callback compliance too.
Also, if the user defines their own adapter, I want to check whether it’s available and if it’s behaviour-compliant. And, if possible, if Req isn’t installed, but a custom adapter is not defined either, automatically installing Req would be nice.
So, what would be the proper way to accomplish all this? I am somewhat experienced in Elixir, but I’ve never had to deal with such problems before. I’ve taken a look at Tesla source code, but it’s different: Tesla checks if an adapter is installed, rather than configured to be used. Also, I want to make it clear that I am going to write proper and explicit documentation, but we all know how this usually goes, so I still want all those compile-time checks.






















