defmodule ConfigHelpers do @moduledoc """ Module to be imported by `runtime.exs` to allow accessing the `get_env` function. Note that functions of this module are not supposed to be called by regular application code since they use `Config.config_env()` which is only possible while reading config files. """ require Config @accepted_true_values ~w(true True TRUE yes YES t T 1) @doc """ 3-arity clause needed to support single default value for all envs together with options list. See `get_env/2` for documentation. """ def get_env(key, default, opts) when not is_list(default) and is_list(opts) do get_env(key, [default: default] ++ opts) end @doc """ Read configuration from an environment variables while providing defaults for some envs. Defaults can be given as keyword list or single value (equivalent to `default: `). If the given `key` is not set in the environment (and not empty, see `allow_empty`), default keys are checked in the following order: 1. Current env, e.g. `:dev`, `:test` or `:prod` 2. `:non_prod`, if current env is `:dev` or `:test` 3. `:default` If no value can be found, `ConfigHelpers.EnvError` is raised. The following options can be added to the defaults list: * `as:` Typecast values to `:integer` or `:boolean` * For `:boolean`, the following values are considered truthy: `#{inspect(@accepted_true_values)}` * For `:integer`, passing a value that fails conversion using `String.to_integer/1` causes `ArgumentError` to be raised. * `allow_empty:` Whether to accept empty strings as being set. By default, empty strings don't count as being set. """ def get_env(key, defaults \\ []) def get_env(key, default) when not is_list(default) do get_env(key, default: default) end def get_env(key, defaults) do case System.fetch_env(key) do {:ok, value} -> if value != "" or Keyword.get(defaults, :allow_empty, false) do value else find_default(key, defaults) end :error -> find_default(key, defaults) end |> maybe_cast(defaults, key) end defp find_default(key, defaults) do env = target_env() with :error <- Keyword.fetch(defaults, env), :error <- maybe_fetch_non_prod(defaults, env), :error <- Keyword.fetch(defaults, :default) do raise __MODULE__.EnvError, var: key, env: env else {:ok, value} -> value end end defp target_env() do Config.config_env() end defp maybe_fetch_non_prod(_defaults, :prod), do: :error defp maybe_fetch_non_prod(defaults, _env), do: Keyword.fetch(defaults, :non_prod) defp maybe_cast(value, options, key) do case Keyword.fetch(options, :as) do {:ok, :integer} -> cast_to_integer(value, key) {:ok, :boolean} -> cast_to_boolean(value) {:ok, other} -> raise "unexpected type specified for 'get_env(..., as:)': #{inspect(other)}" :error -> try_infer_cast(value, options, key) end end defp try_infer_cast(value, options, key) do options # Pick first defaults entry, avoid picking an option. |> Enum.find(fn {key, _value} -> key in ~w(dev test prod non_prod default)a end) |> case do {_key, default} when is_integer(default) -> cast_to_integer(value, key) {_key, default} when is_boolean(default) -> cast_to_boolean(value) _ -> value end end defp cast_to_integer(value, _key) when is_integer(value), do: value defp cast_to_integer(value, key) when is_binary(value) do String.to_integer(value) rescue ArgumentError -> reraise %ArgumentError{ message: "unable to convert #{inspect(value)} to integer for env variable #{inspect(key)}" }, __STACKTRACE__ end defp cast_to_boolean(value) when is_boolean(value), do: value defp cast_to_boolean(value) when is_binary(value), do: value in @accepted_true_values end