defmodule EnvGuard do @external_resource "README.md" @moduledoc "README.md" |> File.read!() |> String.split("") |> 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