defmodule EnvGuard do
@external_resource "README.md"
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC -->")
|> Enum.fetch!(1)
alias EnvGuard.Cast
alias EnvGuard.Constraints
alias EnvGuard.Env
@type constraint :: EnvGuard.Types.constraint()
@type type :: EnvGuard.Types.type()
@doc """
Fetches a required environment variable, casting it to `type` and checking
`constraints`.
Returns the cast value. Raises a `RuntimeError` if the variable is not set,
cannot be cast to `type`, or violates one of the `constraints`.
## Examples
secret_key_base = required("SECRET_KEY_BASE", :string, min_length: 64)
pool_size = required("POOL_SIZE", :integer, min: 1, max: 100)
"""
@spec required(String.t(), type, [constraint]) :: any()
def required(env_name, type, constraints \\ []) do
case test_var(env_name, type, constraints) do
{:ok, value} ->
value
{:error, :env_var_not_set} ->
raise "Environment variable #{env_name} is not set"
{:error, :invalid_type, err} ->
raise "Environment variable #{env_name} is not of type #{inspect(type)}. #{err}"
{:error, :constraint_violation, err} ->
raise "Environment variable #{env_name} does not meet constraints. #{err}"
end
end
@doc """
Fetches an optional environment variable, falling back to `default`.
When the variable is set, it is cast to `type` and checked against
`constraints`, and the cast value is returned. When it is not set — or it is
set but fails casting or a constraint — `default` is returned instead. In the
failure cases a warning is logged via `Logger` before falling back.
## Examples
phx_server = optional("PHX_SERVER", :boolean, false)
log_level = optional("LOG_LEVEL", {:enum, ["debug", "info", "warning"]}, "info")
"""
@spec optional(String.t(), type, default, [constraint]) :: any() | default when default: var
def optional(env_name, type, default, constraints \\ []) do
case test_var(env_name, type, constraints) do
{:ok, value} ->
value
{:error, :env_var_not_set} ->
default
{:error, :invalid_type, err} ->
require Logger
Logger.warning(
"Environment variable #{env_name} is set but not of type #{inspect(type)}. #{err} Falling back to default."
)
default
{:error, :constraint_violation, err} ->
require Logger
Logger.warning(
"Environment variable #{env_name} is set but does not meet constraints. #{err} Falling back to default."
)
default
end
end
# ---------------------------------------------------------------------------#
# Helpers #
# ---------------------------------------------------------------------------#
@spec test_var(String.t(), type, [constraint]) ::
{:ok, any()} | {:error, atom()} | {:error, atom(), String.t()}
defp test_var(name, type, constraints) do
with {:ok, value} <- Env.fetch(name),
{:ok, value} <- Cast.cast(value, type),
{:ok, value} <- Constraints.check_constraints(value, type, constraints) do
{:ok, value}
else
{:error, :constraint_violation, err} ->
{:error, :constraint_violation, err}
{:error, :cast_fail, err} ->
{:error, :invalid_type, err}
{:error, :env_var_not_set} ->
{:error, :env_var_not_set}
end
end
end