The simplest solution to your problem is to define a wrapper module that would explicitly pass runtime configuration to your library:
defmodule MyLibrary.Mailer do
def send(from, to, subject, body, options) do
api_key = Access.fetch!(options, :api_key)
# send email
end
end
defmodule MyApp.Mailer do
def send(from, to, subject, body, options \\ []) do
options = Keyword.merge([api_key: Application.fetch_env!(:my_app, :mailer_api_key)], options)
MyLibrary.Mailer.send(from, to, subject, body, options)
end
end
If you don’t mind using a little metaprogramming magic you can reduce your app’s wrapper module down to a single line at the cost of increased complexity in the library. Something like this:
defmodule MyApp.Mailer do
use MyLibrary.Mailer, mailer_config: {:my_app, :mailer}
end
defmodule MyLibrary.Mailer do
@callback send(from :: String.t(), to :: String.t(), subject :: String.t(), body :: String.t()) ::
:ok | {:error, reason :: term()}
defmacro __using__(opts) do
quote bind_quoted: [behaviour_module: __MODULE__, opts: opts] do
@behaviour_module behaviour_module
@behaviour @behaviour_module
@mailer_config Keyword.fetch!(opts, :mailer_config)
def send(from, to, subject, body) do
@behaviour_module.send(from, to, subject, body, @mailer_config)
end
end
end
def send(from, to, subject, body, {app, key}) do
config = Application.fetch_env!(app, key)
IO.inspect(config)
# send email
end
end