Dotenvy behaviour (Dotenvy v0.1.0) View Source

Dotenvy is an Elixir implementation of the original dotenv Ruby gem.

It assists in setting environment variables in ways that are compatible with both mix releases and with runtime configuration using conventions that should be familiar to developers coming from other languages. Conveniences for reading the variables and converting thier values are included (See Dotenvy/env!/2, Dotenvy.env/3, and Dotenvy.Transformer.to/2).

Unlike other configuration helpers, Dotenvy enforces no convention for the naming of your files: you may name your configuration files whatever you wish. .env and its variants is a common choice, but Dotenvy does not have opinions. You must only pass file paths as arguments to the Dotenvy.source/2 or Dotenvy.source!/2 functions.

Usage Suggestion

Dotenvy is designed to help help set up your application at runtime. However, saying "at runtime" isn't specific enough: an application's configuration must be bootstrapped before it can actually start. Although there are other places where Dotenvy may prove useful, it was designed with the config/runtime.exs in mind: that's where it helps make the hand-off from system environment variables to the application configuration in the cleanest, most declarative way possible.

.env for environment-specific config

It is possible to use only a config.exs and and runtime.exs file to configure many Elixir applications: let the .env files handle any differences between environments. Consider the following setup:

config/config.exs

# compile-time config
import Config

config :myapp,
  ecto_repos: [MyApp.Repo]

config :myapp, MyApp.Repo,
  migration_timestamps: [
    type: :utc_datetime,
    inserted_at: :created_at
  ]

config/runtime.exs

import Config
import Dotenvy

source([".env", ".env.#{config_env()}"])

config :myapp, MyApp.Repo,
  database: env!("DATABASE", :string),
  username: env!("USERNAME", :string),
  password: env!("PASSWORD", :string),
  hostname: env!("HOSTNAME", :string),
  pool_size: env!("POOL_SIZE", :integer),
  adapter: env("ADAPTER", :module, Ecto.Adapters.Postgres),
  pool: env!("POOL", :module?)

.env

  DATABASE=myapp_dev
  USERNAME=myuser
  PASSWORD=mypassword
  HOSTNAME=localhost
  POOL_SIZE=10
  POOL=

.env.test

  DATABASE=myapp_test
  USERNAME=myuser
  PASSWORD=mypassword
  HOSTNAME=localhost
  POOL_SIZE=10
  POOL=Ecto.Adapters.SQL.Sandbox

The above setup would expect .env to be in the .gitignore. The above example demonstrates developer settings appropriate for local development, but a production deployment would only differ in its values: the shape of the file would be the same.

The .env.test file is loaded when running tests, so its values override any of the values set in the .env.

By using Dotenvy.env!/2, there is a strong contract with the environment: the system running this app must have the designated environment variables set somehow, otherwise this app will not start (and a specific error will be raised).

Using the nil-able variants of the type-casting (those ending with ?) is an easy way to defer to default values: env!("POOL", :module?) requires that the POOL variable is set, but it will return a nil if the value is an empty string. See Dotenvy.Transformer for more details.

Note for Mix Tasks

If you have authored your own Mix tasks, you must ensure that they are loading application configuration in a way that is compatible with the runtime config. A good way to do this is to include Mix.Task.run("app.config"), e.g.

def run(_args) do
  Mix.Task.run("app.config")
  # ...
end

Link to this section Summary

Functions

Attempts to read the given system environment variable; if it exists, its value is converted to the given type. If the variable is not found, the provided default is returned.

Reads the given system environment variable and converts its value to the given type. This relies on System.fetch_env!/1 so it will raise if a variable is not set.

Like Bash's source command, this loads the given file(s) and sets the corresponding system environment variables using a side effect function (&System.put_env/1 by default).

As source/2, but returns map on success or raises on error.

Callbacks

A parser implementation should receive the contents read from a file, a map of vars (with string keys, as would come from System.get_env/0), and a keyword list of opts.

Link to this section Functions

Link to this function

env(variable, type, default \\ nil)

View Source

Specs

env(variable :: binary(), type :: atom(), default :: any()) ::
  any() | no_return()

Attempts to read the given system environment variable; if it exists, its value is converted to the given type. If the variable is not found, the provided default is returned.

The default value will not be converted: it will be returned as-is. This allows greater control of the output.

Although this relies on System.fetch_env/1, it may still raise an error if an unsupported type is provided.

Examples

iex> env("PORT", :integer, 5432)
5433
iex> env("NOT_SET", :boolean, %{not: "converted"})
%{not: "converted"}

Specs

env!(variable :: binary(), type :: atom()) :: any() | no_return()

Reads the given system environment variable and converts its value to the given type. This relies on System.fetch_env!/1 so it will raise if a variable is not set.

Examples

iex> env!("PORT", :integer)
5432
iex> env!("ENABLED", :boolean)
true
Link to this function

source(files, opts \\ [])

View Source

Specs

source(files :: binary() | [binary()], opts :: keyword()) ::
  {:ok, %{optional(String.t()) => String.t()}} | {:error, any()}

Like Bash's source command, this loads the given file(s) and sets the corresponding system environment variables using a side effect function (&System.put_env/1 by default).

Files are processed in the order they are given. Values parsed from one file may override values parsed from previous files: the last file parsed has final say. The :overwrite? option determines how the parsed values will be merged with the existing system values.

Options

  • :side_effect an arity 1 function called after the successful parsing of each of the given files. The default is &System.put_env/1, which will have the effect of setting system environment variables based on the results of the file parsing.
  • :overwrite? boolean indicating whether or not values parsed from provided .env files should overwrite existing system environment variables. Default: false
  • :parser module that parses the given file(s). Overridable for testing. Default: Dotenv.Parser
  • :require_files specifies which of the given files (if any) must be present. When true, all the listed files must exist. When false, none of the listed files must exist. When some of the files are required and some are optional, provide a list specifying which files are required.
  • :side_effect an arity 1 function called after the successful parsing of each of the given files. The default is &System.put_env/1, which will have the effect of setting system environment variables based on the results of the file parsing.
  • :vars a map with string keys representing the starting pool of variables. Default: output of System.get_env/0.

Examples

iex> Dotenvy.source(".env")
{:ok, %{
  "PWD" => "/users/home",
  "DATABASE_URL" => "postgres://postgres:postgres@localhost/myapp",
  # ...etc...
  }
}

# If you only want to return the parsed contents of the listed files
# ignoring system environment variables altogether
iex> Dotenvy.source(["file1", "file2"], side_effect: false, vars: %{})
Link to this function

source!(files, opts \\ [])

View Source

Specs

source!(files :: binary() | [binary()], opts :: keyword()) ::
  %{optional(String.t()) => String.t()} | no_return()

As source/2, but returns map on success or raises on error.

Link to this section Callbacks

Link to this callback

parse(contents, vars, opts)

View Source

Specs

parse(contents :: binary(), vars :: map(), opts :: keyword()) ::
  {:ok, map()} | {:error, any()}

A parser implementation should receive the contents read from a file, a map of vars (with string keys, as would come from System.get_env/0), and a keyword list of opts.

This callback is provided to help facilitate testing. See Dotenvy.Parser for the default implementation.