defmodule CozyEnv do
@moduledoc """
Helpers for handling OS environment variables.
It helps to provide:
* user-friendly error messages
* ...
"""
alias CozyEnv.EnvError
@type data_type ::
:boolean
| :integer
| :float
| :string
| :atom
@type result ::
boolean()
| integer()
| float()
| String.t()
| atom()
@type option :: {:message, String.t()}
@type options :: [option()]
@supported_types [
:boolean,
:integer,
:float,
:string,
:atom
]
@doc """
Gets the value of the given environment variable, and casts it into given type.
If the environment variable is not set, returns `nil`.
## Examples
iex> CozyEnv.get_env("NOT_SET", :boolean)
nil
# export PHX_SERVER=true
iex> CozyEnv.get_env("PHX_SERVER", :boolean)
true
# export DB_POOL_SIZE=10
iex> CozyEnv.get_env("DB_POOL_SIZE", :integer)
10
# export PERCENT=0.95
iex> CozyEnv.get_env("PERCENT", :float)
0.95
# export USER=billy
iex> CozyEnv.get_env("USER", :string)
"billy"
# export USER=billy
iex> CozyEnv.get_env("USER", :atom)
:billy
"""
@spec get_env(String.t(), data_type()) :: result()
def get_env(varname, type)
when is_binary(varname) and type in @supported_types do
case System.fetch_env(varname) do
{:ok, value} ->
apply(__MODULE__, :"to_#{type}", [varname, value])
:error ->
nil
end
end
@spec get_env(String.t()) :: String.t()
def get_env(varname), do: get_env(varname, :string)
@doc """
Gets the value of the given environment variable, and casts it into given type.
If the environment variable is not set, raises an error.
## Examples
iex> CozyEnv.fetch_env!("NOT_SET", :boolean)
** (CozyEnv.EnvError) environment variable NOT_SET is missing
(cozy_env) lib/cozy_env.ex:134: CozyEnv.fetch_env!/3
iex:1: (file)
# export PHX_SERVER=true
iex> CozyEnv.fetch_env!("PHX_SERVER", :boolean)
true
# export DB_POOL_SIZE=10
iex> CozyEnv.fetch_env!("DB_POOL_SIZE", :integer)
10
# export PERCENT=0.95
iex> CozyEnv.fetch_env!("PERCENT", :float)
0.95
# export USER=billy
iex> CozyEnv.fetch_env!("USER", :string)
"billy"
# export USER=billy
iex> CozyEnv.fetch_env!("USER", :atom)
:billy
"""
@spec fetch_env!(String.t(), data_type(), options()) :: result()
def fetch_env!(varname, type, opts)
when is_binary(varname) and type in @supported_types and is_list(opts) do
case System.fetch_env(varname) do
{:ok, value} ->
apply(__MODULE__, :"to_#{type}", [varname, value])
:error ->
default_message = "environment variable #{varname} is missing"
extra_message = Keyword.get(opts, :message)
message =
if extra_message do
"""
#{default_message}.
#{extra_message}
"""
else
default_message
end
raise EnvError, message
end
end
@spec fetch_env!(String.t(), data_type()) :: result()
def fetch_env!(varname, type) when is_binary(varname) and is_atom(type) do
fetch_env!(varname, type, [])
end
@spec fetch_env!(String.t(), options()) :: String.t()
def fetch_env!(varname, opts) when is_binary(varname) and is_list(opts) do
fetch_env!(varname, :string, opts)
end
@spec fetch_env!(String.t()) :: String.t()
def fetch_env!(varname) when is_binary(varname) do
fetch_env!(varname, :string, [])
end
@doc false
def to_boolean(_varname, "false"), do: false
def to_boolean(_varname, "0"), do: false
def to_boolean(_varname, ""), do: false
def to_boolean(_varname, _), do: true
@doc false
def to_integer(varname, value) do
try do
String.to_integer(value)
rescue
ArgumentError ->
# credo:disable-for-next-line
raise EnvError,
"environment variable #{varname} is provided, but the value " <>
"#{inspect(value)} is not the string representation of an integer"
end
end
@doc false
def to_float(varname, value) do
case Float.parse(value) do
{float, ""} ->
float
_ ->
# credo:disable-for-next-line
raise EnvError,
"environment variable #{varname} is provided, but the value " <>
"#{inspect(value)} is not the string representation of a float"
end
end
@doc false
def to_string(_varname, value), do: value
@doc false
def to_atom(_varname, value), do: String.to_atom(value)
end
defmodule CozyEnv.EnvError do
defexception [:message]
end