Current section
Files
Jump to
Current section
Files
lib/confex.ex
defmodule Confex do
@moduledoc """
Confex simplifies reading configuration at run-time with adapter-based system for resolvers.
# Configuration tuples
Whenever there is a configuration that should be resolved at run-time you need to replace it's value in `config.exs`
by Confex configuration type. Common structure:
```elixir
@type fetch_statement :: {adapter :: atom() | module(), value_type :: value_type, key :: String.t, default :: any()}
| {value_type :: value_type, key :: String.t}
| {key :: String.t, default :: any()}
| {key :: String.t}
```
If `value_type` is set, Confex will automatically cast it's value. Otherwise, default type of `:string` is used.
| Confex Type | Elixir Type | Description |
| ----------- | ----------------- | ----------- |
| `:string` | `String.t` | Default. |
| `:integer` | `Integer.t` | Parse Integer value in string. |
| `:float` | `Float.t` | Parse Float value in string. |
| `:boolean` | `true` or `false` | Cast 'true', '1', 'yes' to `true`; 'false', '0', 'no' to `false`. |
| `:atom` | `atom()` | Cast string to atom. |
| `:module` | `module()` | Cast string to module name. |
| `:list` | `List.t` | Cast comma-separated string (`1,2,3`) to list (`[1, 2, 3]`). |
Examples:
* `var` - any bare values will be left as-is.
* `{:system, "ENV_NAME", default}` - read string from "ENV_NAME" environment variable or return `default` \
if it's not set or has empty value.
* `{:system, "ENV_NAME"}` - same as above, with default value `nil`.
* `{:system, :integer, "ENV_NAME", default}` - read string from "ENV_NAME" environment variable and cast it \
to integer or return `default` if it's not set or has empty value.
* `{:system, :integer, "ENV_NAME"}` - same as `{:system, :integer, "ENV_NAME", nil}`.
* `{{:via, MyAdapter}, :string, "ENV_NAME", default}` - read value by key "ENV_NAME" via adapter `MyAdapter` \
or return `default` if it's not set or has empty value.
* `{{:via, MyAdapter}, :string, "ENV_NAME"}` - same as above, with default value `nil`.
# Adapters
By-default, you can use `:system` adapter to read configuration from system environment variables.
You can extend it by implementing `Confex.Adapter` behaviour with your own logic.
"""
alias Confex.Resolver
@type configuration_tuple :: {value_type :: Confex.Type.t, key :: String.t, default :: any()}
| {value_type :: Confex.Type.t, key :: String.t}
| {key :: String.t, default :: any()}
| {key :: String.t}
@doc """
Returns the value for key in app’s environment in a tuple.
This function mimics `Application.fetch_env/2` function.
If the configuration parameter does not exist or can not be parsed, the function returns :error.
## Example
iex> :ok = System.put_env("MY_TEST_ENV", "foo")
...> Application.put_env(:myapp, :test_var, {:system, "MY_TEST_ENV"})
...> {:ok, "foo"} = #{__MODULE__}.fetch_env(:myapp, :test_var)
{:ok, "foo"}
iex> :ok = System.delete_env("MY_TEST_ENV")
...> Application.put_env(:myapp, :test_var, {:system, :integer, "MY_TEST_ENV", "bar"})
...> {:ok, "bar"} = #{__MODULE__}.fetch_env(:myapp, :test_var)
{:ok, "bar"}
iex> :ok = System.delete_env("MY_TEST_ENV")
...> Application.put_env(:myapp, :test_var, {:system, :integer, "MY_TEST_ENV"})
...> :error = #{__MODULE__}.fetch_env(:myapp, :test_var)
:error
iex> :ok = System.put_env("MY_TEST_ENV", "foo")
...> Application.put_env(:myapp, :test_var, {:system, :integer, "MY_TEST_ENV"})
...> :error = #{__MODULE__}.fetch_env(:myapp, :test_var)
:error
iex> Application.put_env(:myapp, :test_var, 1)
...> {:ok, 1} = #{__MODULE__}.fetch_env(:myapp, :test_var)
{:ok, 1}
"""
@spec fetch_env(app :: Application.app(), key :: Application.key()) :: {:ok, Application.value()} | :error
def fetch_env(app, key) do
with {:ok, config} <- Application.fetch_env(app, key),
{:ok, config} <- Resolver.resolve(config) do
{:ok, config}
else
:error -> :error
{:error, _reason} -> :error
end
end
@doc """
Returns the value for key in app’s environment.
This function mimics `Application.fetch_env!/2` function.
If the configuration parameter does not exist or can not be parsed, raises `ArgumentError`.
## Example
iex> :ok = System.put_env("MY_TEST_ENV", "foo")
...> Application.put_env(:myapp, :test_var, {:system, "MY_TEST_ENV"})
...> "foo" = #{__MODULE__}.fetch_env!(:myapp, :test_var)
"foo"
iex> :ok = System.delete_env("MY_TEST_ENV")
...> Application.put_env(:myapp, :test_var, {:system, :integer, "MY_TEST_ENV", "bar"})
...> "bar" = #{__MODULE__}.fetch_env!(:myapp, :test_var)
"bar"
iex> :ok = System.delete_env("MY_TEST_ENV")
...> Application.put_env(:myapp, :test_var, {:system, :integer, "MY_TEST_ENV"})
...> #{__MODULE__}.fetch_env!(:myapp, :test_var)
** (ArgumentError) can't fetch value for application :myapp, \
can not resolve key MY_TEST_ENV value via adapter Elixir.Confex.Adapters.SystemEnvironment
iex> :ok = System.put_env("MY_TEST_ENV", "foo")
...> Application.put_env(:myapp, :test_var, {:system, :integer, "MY_TEST_ENV"})
...> #{__MODULE__}.fetch_env!(:myapp, :test_var)
** (ArgumentError) can't fetch value for application :myapp, can not cast "foo" to Integer
iex> Application.put_env(:myapp, :test_var, 1)
...> 1 = #{__MODULE__}.fetch_env!(:myapp, :test_var)
1
"""
@spec fetch_env!(app :: Application.app(), key :: Application.key()) :: Application.value() | no_return
def fetch_env!(app, key) do
config = Application.fetch_env!(app, key)
case Resolver.resolve(config) do
{:ok, config} ->
config
{:error, {_reason, message}} ->
raise ArgumentError, "can't fetch value for application #{inspect app}, #{message}"
end
end
@doc """
Returns the value for key in app’s environment in a tuple.
This function mimics `Application.get_env/2` function.
If the configuration parameter does not exist or can not be parsed, returns default value or `nil`.
## Example
iex> :ok = System.put_env("MY_TEST_ENV", "foo")
...> Application.put_env(:myapp, :test_var, {:system, "MY_TEST_ENV"})
...> "foo" = #{__MODULE__}.get_env(:myapp, :test_var)
"foo"
iex> :ok = System.delete_env("MY_TEST_ENV")
...> Application.put_env(:myapp, :test_var, {:system, :integer, "MY_TEST_ENV", "bar"})
...> "bar" = #{__MODULE__}.get_env(:myapp, :test_var)
"bar"
iex> :ok = System.delete_env("MY_TEST_ENV")
...> Application.put_env(:myapp, :test_var, {:system, :integer, "MY_TEST_ENV"})
...> nil = #{__MODULE__}.get_env(:myapp, :test_var)
nil
iex> :ok = System.delete_env("MY_TEST_ENV")
...> Application.put_env(:myapp, :test_var, {:system, :integer, "MY_TEST_ENV"})
...> "baz" = #{__MODULE__}.get_env(:myapp, :test_var, "baz")
"baz"
iex> :ok = System.put_env("MY_TEST_ENV", "foo")
...> Application.put_env(:myapp, :test_var, {:system, :integer, "MY_TEST_ENV"})
...> nil = #{__MODULE__}.get_env(:myapp, :test_var)
nil
iex> nil = #{__MODULE__}.get_env(:myapp, :does_not_exist)
nil
iex> Application.put_env(:myapp, :test_var, 1)
...> 1 = #{__MODULE__}.get_env(:myapp, :test_var)
1
"""
@spec get_env(app :: Application.app(), key :: Application.key()) :: {:ok, Application.value()} | {:error, any()}
def get_env(app, key, default \\ nil) do
with {:ok, config} <- Application.fetch_env(app, key),
{:ok, config} <- Resolver.resolve(config) do
config
else
:error -> default
{:error, _reason} -> default
end
end
@doc """
Recursively merges configuration with default values.
Both values must be either in `Keyword` or `Map` structures, otherwise ArtumentError is raised.
## Example
iex> [b: 3, a: 1] = #{__MODULE__}.merge_configs!([a: 1], [a: 2, b: 3])
[b: 3, a: 1]
iex> %{a: 1, b: 3} = #{__MODULE__}.merge_configs!(%{a: 1}, %{a: 2, b: 3})
%{a: 1, b: 3}
iex> #{__MODULE__}.merge_configs!(%{a: 1}, [b: 2])
** (ArgumentError) can not merge default values [b: 2] with configuration %{a: 1} because their types mismatch, \
expected both to be either Map or Keyword structures
"""
@spec merge_configs!(config :: Keyword.t | Map.t, defaults :: Keyword.t | Map.t) :: Keyword.t | Map.t
def merge_configs!(config, []),
do: config
def merge_configs!(nil, defaults),
do: defaults
def merge_configs!(config, defaults) do
cond do
Keyword.keyword?(config) and Keyword.keyword?(defaults) ->
defaults
|> Keyword.merge(config, &compare/3)
|> Resolver.resolve!()
is_map(config) and is_map(defaults) ->
defaults
|> Map.merge(config, &compare/3)
|> Resolver.resolve!()
true ->
raise ArgumentError,
"can not merge default values #{inspect defaults} " <>
"with configuration #{inspect config} because their types mismatch, " <>
"expected both to be either Map or Keyword structures"
end
end
defp compare(_k, v1, v2) do
if is_map(v2) or Keyword.keyword?(v2),
do: merge_configs!(v1, v2),
else: v2
end
# Helper to include configuration into module and validate it at compile-time/run-time.
@doc false
defmacro __using__(opts) do
quote bind_quoted: [opts: opts], location: :keep do
@otp_app Keyword.fetch!(opts, :otp_app)
@module_config_defaults Keyword.delete(opts, :otp_app)
@doc """
Returns module configuration.
If application environment contains values in `Keyword` or `Map` struct,
defaults from macro usage will be recursively merged with application configuration.
If one of the configuration parameters does not exist or can not be resolved, raises `ArgumentError`.
"""
@spec config() :: any()
def config do
@otp_app
|> Confex.get_env(__MODULE__)
|> Confex.merge_configs!(@module_config_defaults)
|> Confex.Resolver.resolve!()
|> validate_config!()
end
@spec validate_config!(config :: any()) :: any()
def validate_config!(config),
do: config
defoverridable [validate_config!: 1]
end
end
end