README.md

# Skogsrå

[![Build Status](https://travis-ci.org/gmtprime/skogsra.svg?branch=master)](https://travis-ci.org/gmtprime/skogsra) [![Hex pm](http://img.shields.io/hexpm/v/skogsra.svg?style=flat)](https://hex.pm/packages/skogsra) [![hex.pm downloads](https://img.shields.io/hexpm/dt/skogsra.svg?style=flat)](https://hex.pm/packages/skogsra)

> The _Skogsrå_ was a mythical creature of the forest that appears in the form
> of a small, beautiful woman with a seemingly friendly temperament. However,
> those who are enticed into following her into the forest are never seen
> again.

This library attempts to improve the use of OS environment variables for
application configuration:

* Variable defaults.
* Automatic type casting of values.
* Automatic documentation generation for variables.
* Runtime reloading.
* Setting variable's values at runtime.
* Fast cached values access by using `:persistent_term` as temporal storage.
* YAML configuration provider for Elixir releases.

## Small example

You would create a config module e.g:

```elixir
defmodule MyApp.Config do
  use Skogsra

  @envdoc "My hostname"
  app_env :my_hostname, :myapp, :hostname,
    default: "localhost"
end
```

Calling `MyApp.Config.my_hostname()` will retrieve the value for the
hostname in the following order:

1. From the OS environment variable `$MYAPP_HOSTNAME` (can be overriden by the
   option `os_env`).
2. From the configuration file e.g:
     ```
     config :myapp,
       hostname: "my.custom.host"
     ```
3. From the default value if it exists (In this case, it would return
   `"localhost"`).

## Available options

There are several options for configuring an environment variables:

Option        | Type                 | Default       | Description
:------------ | :------------------- | :------------ | :----------
`default`     | `any`                | `nil`         | Sets the Default value for the variable.
`type`        | `Skogsra.Env.type()` | `:binary`     | Sets the explicit type for the variable.
`os_env`      | `binary`             | autogenerated | Overrides automatically generated OS environment variable name.
`skip_system` | `boolean`            | `false`       | Whether it should skip looking for a value in the OS or not.
`skip_config` | `boolean`            | `false`       | Whether it should skip looking for a value in the application config or not.
`required`    | `boolean`            | `false`       | Whether the variable is required or not.
`cached`      | `boolean`            | `true`        | Whether the variable should be cached or not.

Additional topics:

- [Automatic type casting](#automatic-type-casting).
- [Explicit type casting](#explicit-type-casting).
- [Explicit OS environment variable names](#explicit-os-environment-variable-name).
- [Required variables](#required-variables).
- [Caching variables](#caching-variables).
- [Handling different environments](#handling-different-environments).
- [Setting and reloading variables](#setting-and-reloading-variables).
- [YAML Config Provider](#yaml-config-provider).
- [Using with Hab](#using-with-hab).
- [Installation](#installation).

## Automatic type casting

If the `default` value is set (and no explicit `type` is found), the variable
value will be casted as the same type of the default value. For this to work,
the default value should be of the following types:

- `:binary`
- `:integer`
- `:float`.
- `:boolean`
- `:atom`

e.g. in the following example, the value will be casted to `:atom`
automatically:

```elixir
defmodule MyApp.Config do
  use Skogsra

  @envdoc "My environment"
  app_env :my_environmen, :myapp, :environment,
    default: :prod
end
```

If either of the system OS or the application environment variables are defined,
`Skogsra` will try to cast their values to the default value's type which it
`atom` e.g:

```elixir
iex(1)> System.get_env("MYAPP_ENVIRONMENT")
"staging"
iex(2)> MyApp.Config.my_environment()
{:ok, :staging}
```

or

```elixir
iex(1)> Application.get_env(:myapp, :environment)
"staging"
iex(2)> MyApp.Config.my_environment()
{:ok, :staging}
```

> **Note**: If the variable is already of the desired type, it won't be casted.

## Explicit type casting

A type can be explicitly set. The available types are:
 - `:binary` (default).
 - `:integer`.
 - `:float`.
 - `:boolean`.
 - `:atom`.
 - `:module` (modules loaded in the system).
 - `:unsafe_module` (modules that might or might not be loaded in the system)
 - A module name with an implementation for the behaviour `Skogsra.Type`.

e.g. a possible implementation for casting `"1, 2, 3, 4"` to `[integer()]`
would be:

```elixir
defmodule MyList do
  use Skogsra.Type

  def cast(value) when is_binary(value) do
    list =
      value
      |> String.split(~r/,/)
      |> Stream.map(&String.trim/1)
      |> Enum.map(String.to_integer/1)
    {:ok, list}
  end

  def cast(value) when is_list(value) do
    if Enum.all?(value, &is_integer/1), do: {:ok, value}, else: :error
  end

  def cast(_) do
    :error
  end
end
```

If then we define the following enviroment variable with `Skogsra`:

```elixir
defmodule MyApp.Config do
  use Skogsra

  app_env :my_integers, :myapp, :integers,
    type: MyList
end
```

If either of the system OS or the application environment variables are defined,
`Skogsra` will try to cast their values using our implementation e.g:

```elixir
iex(1)> System.get_env("MYAPP_INTEGERS")
"1, 2, 3"
iex(2)> MyApp.Config.my_integers()
{:ok, [1, 2, 3]}
```

or

```elixir
iex(1)> Application.get_env(:myapp, :integers)
[1, 2, 3]
iex(2)> MyApp.Config.my_integers()
{:ok, [1, 2, 3]}
```

> **Important**: The `default` value is not cast according to `type`.

## Explicit OS environment variable names

Though `Skogsra` automatically generates the names for the OS environment
variables, they can be overriden by using the option `os_env` e.g:

```elixir
defmodule MyApp.Config do
  use Skogsra

  app_env :db_hostname, :myapp, [:postgres, :hostname],
    os_env: "PGHOST"
end
```

This will override the value `$MYAPP_POSTGRES_HOSTNAME` with `$PGHOST` e.g:

```elixir
iex(1)> System.get_env("MYAPP_POSTGRES_HOSTNAME")
"unreachable"
iex(2)> System.get_env("PGHOST")
"reachable"
iex(3)> MyApp.Config.db_hostname()
{:ok, "reachable"}
```

## Required variables

It is possible to set a environment variable as required with the `required`
option e.g:

```elixir
defmodule MyApp.Config do
  use Skogsra

  @envdoc "My port"
  app_env :my_port, :myapp, :port,
    required: true
end
```

If none of the system OS or the application environment variables are defined,
`Skogsra` will return an error e.g:

```elixir
iex(1)> System.get_env("MYAPP_PORT")
nil
iex(2)> Application.get_env(:myapp, :port)
nil
iex(2)> MyApp.Config.my_port()
{:error, "Variable port in app myapp is undefined"}
```

## Handling different environments

If it's necessary to keep several environments, it's possible to use a
`namespace` e.g. given the following variable:

```elixir
defmodule MyApp.Config do
  use Skogsra

  @envdoc "My port"
  app_env :my_port, :myapp, :port,
    default: 4000
end
```

Calling `MyApp.Config.my_port(Test)` will retrieve the value for the hostname
in the following order:

1. From the OS environment variable `$TEST_MYAPP_PORT`.
2. From the configuration file e.g:
    ```
    config :myapp, Test,
      port: 4001
    ```
3. From the OS environment variable `$MYAPP_PORT`.
4. From the configuraton file e.g:
    ```
    config :myapp,
      port: 80
    ```
5. From the default value if it exists. In our example, `4000`.

The ability of loading different environments allows us to do the following
with our configuration file:

```elixir
# file: config/config.exs
use Mix.Config

config :myapp, Prod
  port: 80

config :myapp, Test,
  port: 4001

config :myapp,
  port: 4000
```

While our `Skogsra` module would look like:

```elixir
defmodule MyApp.Config do
  use Skogsra

  @envdoc "Application environment"
  app_env :env, :myapp, :environment,
    type: :unsafe_module

  @envdoc "Application port"
  app_env :port, :myapp, :port,
    default: 4000
end
```

The we can retrieve the values depending on the value of the OS environment
variable `$MYAPP_ENVIRONMENT`:

```elixir
...
with {:ok, env} <- MyApp.Config.env(),
     {:ok, port} <- Myapp.Config.port(env) do
  ... do something with the port ...
end
...
```

## Caching variables

By default, `Skogsra` caches the values of the variables using
`:persistent_term` Erlang module. This makes reads very fast, but **writes are
very slow**.

So avoid setting or reloading values to avoid performance issues (see
[Setting and reloading variables](#setting-and-reloading-variables)).

If you don't want to cache the values, you can set it to `false`:

```elixir
defmodule MyApp.Config do
  use Skogsra

  app_env :value, :myapp, :value,
    cached: false
end
```

## Setting and reloading variables

Every variable definition will generate two additional functions for setting
and reloading the values e.g:

```elixir
defmodule MyApp.Config do
  use Skogsra

  @envdoc "My port"
  app_env :my_port, :myapp, :port,
    default: 4000
end
```

will have the functions:

- `MyApp.Config.put_my_port/1` for setting the value of the variable at
  runtime.
- `MyApp.Config.reload_my_port/` for reloading the value of the variable at
  runtime.

## YAML Config Provider

`Skogsra` includes a simple YAML configuration provider compatible with
`mix release` for Elixir ≥ 1.9.

The following is the supported configuration format:

```yaml
# file: /etc/my_app/config.yml
- app: "my_app"              # Name of the application.
  module: "MyApp.Repo"       # Optional module/namespace.
  config:                    # Actual configuration.
    - database: "my_app_db"
      username: "postgres"
      password: "postgres"
      hostname: "localhost"
      port: 5432
```

The previous configuration file would translate to:

```elixir
config :my_app, MyApp.Repo,
  database: "my_App_db",
  username: "postgres"
  password: "postgres"
  hostname: "localhost"
  port: 5432
```

For using this config provider, just add the following to your release
configuration:

```elixir
config_providers: [{Skogsra.Provider.Yaml, ["/path/to/config/file.yml"]}]
```

> **Note**: If the `module` you're using in you're config does not exist, then
> change it to `namespace` e.g: `namespace: "MyApp.Repo"`. Otherwise, it will
> fail loading it.

## Using with `Hab`

[_Hab_](https://github.com/alexdesousa/hab) is an
[Oh My ZSH](https://github.com/robbyrussell/oh-my-zsh) plugin for loading OS
environment variables automatically.

By default, `Hab` will try to load `.envrc` file, but it's possible to have
several of those files for different purposes e.g:

- `.envrc.prod` for production OS variables.
- `.envrc.test` for testing OS variables.
- `.envrc` for development variables.

`Hab` will load the development variables by default, but it can load the
other files using the command `hab_load <extension>` e.g. loading
`.envrc.prod` would be as follows:

```bash
~/my_project $ hab_load prod
[SUCCESS]  Loaded hab [/home/user/my_project/.envrc.prod]
```

## Installation

The package can be installed by adding `skogsra` to your list of dependencies
in `mix.exs`.

- For Elixir ≥ 1.8 and Erlang ≥ 22

  ```elixir
  def deps do
    [{:skogsra, "~> 2.0"}]
  end
  ```

- For Elixir ≥ 1.9, Erlang ≥ 22 and YAML config provider support:

  ```elixir
  def deps do
    [
      {:skogsra, "~> 2.0"},
      {:yamerl, "~> 0.7"}
    ]
  end
  ```

## Author

Alexander de Sousa.

## License

_Skogsrå_ is released under the MIT License. See the LICENSE file for further
details.