defmodule ExConfigurator do
@moduledoc ~S"""
# ExConfigurator
A simple code generator that reduces the amount of env var retrieval in your app.
## Usage:
It will allow you to simply setup:
defmodule SomeModule do
use ExConfigurator, :some_module
end
and any configs that are defined as:
config :your_app, :some_module, a: 1, b:2
Will autogenerate 2 getters for each element in the keyword list.
* First - `SomeModule.get_a()/0` - Compile time. This will return 1 in the above example.
It will generate a function that will return the compile time value.
* Second - Runtime value. `SomeModule.get_env_a()/0` - This will return 1 in the above example.
But if changed by a runtime config or a environment based config, the new value will be used.
NOTE: You are probably looking for the second and not the first function **wink**
### Configuration
To configure you must add the some config to your application:
config :ex_configurator, application: :your_app
Please replace :your_app with your app name.
### Complex Usage
Nested keyword lists will create env vars for the top level and sub any nested config.
For the following
defmodule SomeNestedModule do
use ExConfigurator, :some_module
end
with any configs that are defined as:
config :your_app, :some_module, keys: [ infura: "asdfsdf", twilio: "234234" ]
The following functions will be generated:
* `SomeModule.get_env_keys()/0` = [ infura: "asdfsdf", twilio: "234234" ] - changes if updated during runtime
* `SomeModule.get_keys()/0` = [ infura: "asdfsdf", twilio: "234234" ]
* `SomeModule.get_env_keys_infura()/0` = "asdfsdf" - changes if updated during runtime
* `SomeModule.get_keys_infura(()/0` = "asdfsdf"
* `SomeModule.get_env_keys_twilio()/0` = "234234" - changes if updated during runtime
* `SomeModule.get_keys_twilio(()/0` = "234234"
This gives lots of flexibility.
## Special forms
There are some special cases you might want to use this:
If a compile time config is set to a tuple starting with `:system` of form: {:system, :integer | :string, "MY_ENV", 3434}
then when calling `SomeModule.get_keys/0` the replaced method will lookup the system environment variable: "MY_ENV" and cast it to
either string or integer value. No `get_env_keys/0` method will be generated in this case.
"""
require Logger
def handle_config(application, path, config_name, var, submodule) when is_map(var) do
quoted = generate_config_function(application, path, config_name, var, submodule)
sub_quoted =
for {inner_config, struct_val} <- var do
handle_config(application, path ++ [config_name], inner_config, struct_val, submodule)
end
[quoted | sub_quoted]
end
def handle_config(application, path, config_name, var, submodule) when is_list(var) do
quoted = generate_config_function(application, path, config_name, var, submodule)
sub_quoted =
for {inner_config, var} <- var do
handle_config(application, path ++ [config_name], inner_config, var, submodule)
end
[quoted | sub_quoted]
end
def handle_config(application, path, config_name, var, submodule),
do: generate_config_function(application, path, config_name, var, submodule)
def generate_config_function(_application, _path, "", _var, _submodule), do: :noop
def generate_config_function(application, path, config_name, var, submodule) do
filtered_path = Enum.filter(path, &(&1 != "")) ++ [config_name]
path_name =
filtered_path
|> Enum.join("_")
generate_config_function_inner(application, path_name, var, submodule, filtered_path)
end
def clean_default_int(int) when is_binary(int), do: int
def clean_default_int(int) when is_integer(int), do: Integer.to_string(int)
def clean_default_string(string), do: to_string(string)
def generate_config_function_inner(
_application,
path_name,
{:system, :integer, name, default_val},
_submodule,
_path
)
when is_binary(name) do
quote do
def unquote(:"get_#{path_name}")() do
{default_int, rest} =
unquote(name)
|> System.get_env(unquote(clean_default_int(default_val)))
|> Integer.parse()
default_int
end
end
end
def generate_config_function_inner(
_application,
path_name,
{:system, :string, name, default_val},
_submodule,
_path
)
when is_binary(name) do
quote do
def unquote(:"get_#{path_name}")() do
unquote(name)
|> System.get_env(unquote(clean_default_string(default_val)))
end
end
end
def generate_config_function_inner(application, path_name, var, submodule, path)
when is_list(var) do
quote do
def unquote(:"get_#{path_name}")() do
unquote(var |> Enum.map(&Macro.escape/1))
end
def unquote(:"get_env_#{path_name}")() do
Enum.reduce(
unquote(path |> Enum.map(&Macro.escape/1)),
Application.get_env(unquote(application), unquote(submodule), []),
fn x, acc ->
case acc do
subj when is_map(subj) -> Map.get(subj, x)
[{_key, _value} | _rest] = subj -> Keyword.get(subj, x)
subj when is_list(subj) -> subj
end
end
)
end
end
end
def generate_config_function_inner(application, path_name, var, submodule, path) do
quote do
def unquote(:"get_#{path_name}")(), do: unquote(Macro.escape(var))
def unquote(:"get_env_#{path_name}")() do
Enum.reduce(
unquote(path |> Enum.map(&Macro.escape/1)),
Application.get_env(unquote(application), unquote(submodule), []),
fn x, acc ->
case acc do
subj when is_map(subj) -> Map.get(subj, x)
subj when is_list(subj) -> Keyword.get(subj, x)
end
end
)
end
end
end
@spec get_application_arg(any) :: atom | nil
def get_application_arg([{:app, application} | _rest]) when is_atom(application),
do: application
def get_application_arg([_current | rest]), do: get_application_arg(rest)
def get_application_arg(_), do: nil
@spec app_or_env(any) :: atom | nil
def app_or_env(args) do
case get_application_arg(args) do
nil -> Application.get_env(:ex_configurator, :application, nil)
app when is_atom(app) -> app
end
end
@spec get_alias(atom(), atom() | keyword()) :: atom()
def get_alias(_caller, name) when is_atom(name), do: name
def get_alias(_caller, [{:name, name} | _rest]) when is_atom(name), do: name
def get_alias(caller, [_first | rest]), do: get_alias(caller, rest)
def get_alias(caller, _), do: caller
defmacro __using__(args \\ nil) do
app = app_or_env(args)
submodule = get_alias(__CALLER__.module, args)
configs = Application.get_env(app, submodule, [])
handle_config(app, [], "", configs, submodule)
end
end