cheatsheet.cheatmd

# Cheatsheet

Quick reference for using Dotenvy.

## Setup
{: .col-3}

### envs/ Directory

The recommended location for storing your `.env` files is inside a dedicated `envs/` directory.
Code editors recognize the `.env` extension.

#### Example envs/dev.env

```env
LOG_LEVEL=debug
AWS_REGION="us-east-1"
HTTP_CLIENT=HTTPoison
```

### Version Control

`envs/dev.env` is tracked by Git. Use it to store sensible non-sensitive defaults.

`envs/dev.local.env` is ignored by Git. Use it to override any variables in the tracked version.

This must match the values used in the runtime configuration.

#### .gitignore

```
*.local.env
```

### Release Compatibility

For compatibility with [releases](https://hexdocs.pm/mix/Mix.Release.html), 
configure your builds to copy (i.e. "overlay") the contents of your `envs/` 
directory into the root of the release.

#### mix.exs

```elixir
defp releases do
[
    my_app: [
    include_executables_for: [:unix],
    steps: [:assemble, :tar],
    overlays: ["envs/", "priv/", "config/"]
    ]
]
end
```

### Runtime Config

Your `config/runtime.exs` is where you source your environment variables. 
The last argument to `Dotenvy.source/2` or `Dotenvy.source!/2` takes precedence.
It's common to use `System.get_env()` as the final argument so any existing system
environment variables will take precedence over anything parsed from the `.env` files.

#### config/runtime.exs

```elixir
import Config
import Dotenvy

dir = System.get_env("RELEASE_ROOT") || "envs/"
# In an umbrella app:
# dir = System.get_env("RELEASE_ROOT") || Path.expand("./envs/") <> "/"
  
source!([
  "#{dir}#{config_env()}.env",
  "#{dir}#{config_env()}.local.env",
  System.get_env()
])
```

## Example Database Configuration
{: .col-3}

### Dev ENV

#### envs/dev.env

```
PG_USERNAME=postgres
PG_PASSWORD=postgres
PG_HOSTNAME=localhost
PG_PORT=5432
PG_DATABASE=m_app_dev
PG_POOL_SIZE=10
PG_POOL=DBConnection.ConnectionPool
PG_SSL=true
```

### Test ENV

#### envs/test.env

```
PG_USERNAME=postgres
PG_PASSWORD=postgres
PG_HOSTNAME=localhost
PG_PORT=5432
PG_DATABASE=my_app_test
PG_POOL_SIZE=10
PG_POOL=Ecto.Adapters.SQL.Sandbox
PG_SSL=false
```

### Runtime Configuration

#### config/runtime.exs

```
import Config
import Dotenvy

dir = System.get_env("RELEASE_ROOT") || "envs/"
  
source!([
  "#{dir}#{config_env()}.env",
  "#{dir}#{config_env()}.local.env",
  System.get_env()
])

config :my_app, MyApp.PGRepo,
  pool: env!("PG_POOL", :module?),
  pool_size: env!("PG_POOL_SIZE", :integer),
  database: env!("PG_DATABASE", :string),
  username: env!("PG_USERNAME", :string),
  password: env!("PG_PASSWORD", :string),
  port: env!("PG_PORT", :integer),
  hostname: env!("PG_HOSTNAME", :string)
```

## Transformations

System Environment variables are always stored as strings which may need to be 
transformed into native Elixir data types.

#### Used as the 2nd argument to `Dotenvy.env!/2` and `Dotenvy.env!/3`

| Conversion Type   | Elixir Type   | On Empty String   |
| --                | --            | --                |
| `:atom`           | atom          | `:""`             |
| `:atom?`          | atom          | `nil`             |
| `:atom!`          | atom          | raise ⚠           |
| `:boolean`        | boolean       | `false`           |
| `:boolean?`       | boolean       | `nil`             |
| `:boolean!`       | boolean       | raise ⚠           |
| `:charlist`       | charlist      | `''` i.e. `[]`    |
| `:charlist?`      | charlist      | `nil`             |
| `:charlist!`      | charlist      | raise ⚠           |
| `:integer`        | integer       | `0`               |
| `:integer?`       | integer       | `nil`             |
| `:integer!`       | integer       | raise ⚠           |
| `:float`          | float         | `0`               |
| `:float?`         | float         | `nil`             |
| `:float!`         | float         | raise ⚠           |
| `:existing_atom`  | atom          | `:""` or raise    |
| `:existing_atom?` | atom          | `nil`             |
| `:existing_atom!` | atom          | raise ⚠           |
| `:module`         | atom          | `:"Elixir."`      |
| `:module?`        | atom          | `nil`             |
| `:module!`        | atom          | raise ⚠           |
| `:string`         | String        | `""`              |
| `:string?`        | String        | `nil`             |
| `:string!`        | String        | raise ⚠           |

Custom functions handle their own behavior.

#### Custom Function Example

```env
PHX_IP="0, 0, 0, 0, 0, 0, 0, 0"
```

```elixir
config :feenix, FeenixWeb.Endpoint,
    http: [
      # Enable IPv6 and bind on all interfaces.
      ip: env!("PHX_IP", fn ip -> 
        ip 
        |> String.split(",") 
        |> Enum.map(&String.trim/1) 
        |> Enum.map(&String.to_integer/1) 
        |> List.to_tuple()
      end)
    ],
```

Your custom functions can raise a `Dotenvy.Error` to benefit from improved messages
that include helpful context about any problems, e.g.

```elixir
strict_boolean! = fn
  "true" -> true
  "false" -> false
  _ ->
    raise Dotenvy.Error,
      message: "strict_boolean! values must be either true or false"
end

config :myapp, :some_bool, env!("SOME_BOOL", strict_boolean!)
```

This will yield an error like the following:

```
** (RuntimeError) Error converting variable SOME_BOOL using custom function: strict_boolean! values must be either true or false
```