Current section
Files
Jump to
Current section
Files
lib/confex.ex
defmodule Confex do
@moduledoc """
This is helper module that provides a nice way to read environment configuration at runtime.
"""
@doc """
Fetches a value from the config, or from the environment if {:system, "VAR"} is provided.
An optional default value and it's type can be provided if desired.
## Example
iex> {test_var, expected_value} = System.get_env |> Enum.take(1) |> List.first
...> Application.put_env(:myapp, :test_var, {:system, test_var})
...> ^expected_value = #{__MODULE__}.get(:myapp, :test_var)
...> :ok
:ok
iex> Application.put_env(:myapp, :test_var2, 1)
...> 1 = #{__MODULE__}.get(:myapp, :test_var2)
1
iex> System.delete_env("TEST_ENV")
...> Application.put_env(:myapp, :test_var2, {:system, :integer, "TEST_ENV", "default_value"})
...> "default_value" = #{__MODULE__}.get(:myapp, :test_var2)
...> System.put_env("TEST_ENV", "123")
...> 123 = #{__MODULE__}.get(:myapp, :test_var2)
123
iex> :default = #{__MODULE__}.get(:myapp, :missing_var, :default)
:default
"""
@spec get(atom, atom, term | nil) :: term
def get(app, key, default \\ nil) when is_atom(app) and is_atom(key) do
app
|> Application.get_env(key)
|> get_value()
|> set_default(default)
end
@doc """
Same as `get/3`, but when you has map.
## Example
iex> {test_var, expected_value} = System.get_env |> Enum.take(1) |> List.first
...> Application.put_env(:myapp, :test_var, [test: {:system, test_var}])
...> [test: ^expected_value] = #{__MODULE__}.get_map(:myapp, :test_var)
...> :ok
:ok
iex> Application.put_env(:myapp, :test_var2, [test: 1])
...> #{__MODULE__}.get_map(:myapp, :test_var2)
[test: 1]
iex> :default = #{__MODULE__}.get_map(:myapp, :other_missing_var, :default)
:default
iex> Application.put_env(:myapp, :test_var3, [test: nil])
...> [test: nil] = #{__MODULE__}.get_map(:myapp, :test_var3)
[test: nil]
"""
@spec get_map(atom, atom, term | nil) :: Keyword.t
def get_map(app, key, default \\ nil) when is_atom(app) and is_atom(key) do
app
|> Application.get_env(key)
|> prepare_list()
|> set_default(default)
end
@doc """
Receives Keyword list with Confex tuples and replaces them with an environment values.
Useful when you want to store configs not in `config.exs`.
# Example
iex> [test: "defaults"] = #{__MODULE__}.process_env([test: {:system, "some_test_var", "defaults"}])
[test: "defaults"]
"""
@spec process_env(Keyword.t | atom | String.t | Integer.t) :: term
def process_env(conf) when is_list(conf),
do: prepare_list(conf)
def process_env(conf),
do: get_value(conf)
# Helpers to work with map values
defp prepare_list(map, converter \\ &get_value/1)
defp prepare_list(nil, _converter),
do: nil
defp prepare_list(map, converter),
do: Enum.map(map, &prepare_list_element(&1, converter))
defp prepare_list_element({key, value}, converter) when is_list(value) and key != :system,
do: {key, prepare_list(value, converter)}
defp prepare_list_element({key, value}, converter) when key != :system,
do: {key, converter.(value)}
defp prepare_list_element(value, _converter),
do: get_value(value)
# Helpers to parse value from supported definition tuples
defp get_value({:system, type, var_name, default_value}) when is_atom(type) do
var_name
|> System.get_env
|> cast(type, var_name)
|> set_default(default_value)
end
defp get_value({:system, :string, var_name}),
do: get_value({:system, :string, var_name, nil})
defp get_value({:system, :integer, var_name}),
do: get_value({:system, :integer, var_name, nil})
defp get_value({:system, :float, var_name}),
do: get_value({:system, :float, var_name, nil})
defp get_value({:system, :boolean, var_name}),
do: get_value({:system, :boolean, var_name, nil})
defp get_value({:system, :atom, var_name}),
do: get_value({:system, :atom, var_name, nil})
defp get_value({:system, :module, var_name}),
do: get_value({:system, :module, var_name, nil})
defp get_value({:system, :list, var_name}),
do: get_value({:system, :list, var_name, nil})
defp get_value({:system, var_name, default_value}),
do: get_value({:system, :string, var_name, default_value})
defp get_value({:system, var_name}),
do: get_value({:system, :string, var_name, nil})
defp get_value(val),
do: val
# Helpers to cast value to correct type
defp cast(nil, _, _var_name),
do: nil
defp cast(value, :integer, var_name) do
case Integer.parse(value) do
{int, _} ->
int
:error ->
raise ArgumentError, "Environment variable #{inspect var_name} can not be parsed as integer. " <>
"Got value: #{inspect value}"
end
end
defp cast(value, :float, var_name) do
case Float.parse(value) do
{int, _} ->
int
:error ->
raise ArgumentError, "Environment variable #{inspect var_name} can not be parsed as float. " <>
"Got value: #{inspect value}"
end
end
defp cast(value, :atom, _var_name) do
value
|> String.to_char_list()
|> List.to_atom()
end
defp cast(value, :module, _var_name) do
Module.concat([value])
end
defp cast(value, :string, _var_name) do
to_string(value)
end
@boolean_true ["true", "1", "yes"]
@boolean_false ["false", "0", "no"]
defp cast(value, :boolean, var_name) when is_binary(value) do
dc_val = String.downcase(value)
cond do
Enum.member?(@boolean_true, dc_val) ->
true
Enum.member?(@boolean_false, dc_val) ->
false
true ->
raise ArgumentError, "Environment variable #{inspect var_name} can not be parsed as boolean. " <>
"Expected 'true', 'false', '1', '0', 'yes' or 'no', got: #{inspect value}"
end
end
@list_separator ","
defp cast(value, :list, _var_name) when is_binary(value) do
value
|> String.split(@list_separator)
|> Enum.map(&String.trim/1)
end
# Set default value from `get` and `get_map` methods.
# Basically we override all nil's with defaults.
defp set_default(nil, default), do: default
defp set_default(val, _), do: val
# Helper to include configs into module and validate it at compile-time/run-time
@doc false
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@dialyzer {:nowarn_function, add_defaults: 2}
@otp_app Keyword.get(opts, :otp_app)
@module_config_overrides Keyword.delete(opts, :otp_app)
@doc """
Returns module configuration.
"""
@spec config() :: Map.t
def config do
@otp_app
|> Confex.get_map(__MODULE__)
|> add_defaults(@module_config_overrides)
|> validate_config
end
defp add_defaults(conf, nil),
do: Confex.process_env(conf)
defp add_defaults(nil, defaults),
do: Confex.process_env(defaults)
defp add_defaults(conf, defaults) do
defaults
|> Keyword.merge(conf, &merge_recursive/3)
|> Confex.process_env
end
defp merge_recursive(_k, v1, v2) do
case is_list(v2) do
true ->
Keyword.merge(v1, v2, &merge_recursive/3)
false ->
v2
end
end
@spec validate_config(config :: Map.t) :: Map.t
def validate_config(conf),
do: conf
defoverridable [validate_config: 1]
end
end
end