README.md

# Mahaul

[![hex.pm](https://img.shields.io/hexpm/v/mahaul.svg)](https://hex.pm/packages/mahaul)
[![hex.pm](https://img.shields.io/hexpm/l/mahaul.svg)](https://hex.pm/packages/mahaul)
[![Coverage Status](https://coveralls.io/repos/github/emadalam/mahaul/badge.svg?branch=main)](https://coveralls.io/github/emadalam/mahaul?branch=main)


Parse and validate your environment variables easily in Elixir with the following benefits.

* Compile time access guarantees
* Parsed values with accurate elixir data types
* Validation of required values before app boot
* `mix` environment specific defaults and fallbacks
* Code autocompletion (if using elixir language server)

![Screenshot](assets/images/autocomplete.png)

[Read more](#why-this-package) for understanding why to use this package and its benefits. The complete documentation for `mahaul` is [available online at HexDocs](https://hexdocs.pm/mahaul).

## Requirements

Elixir 1.13+

## Installation

- Add `mahaul` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:mahaul, "~> 0.6.0"}
  ]
end
```

- Run `mix deps.get`

- Add config in your `config/config.exs` file

```elixir
config :mahaul, mix_env: Mix.env()
```

## Getting started

#### Define the environment variables needed in your app

```elixir
defmodule MyApp.Env do
  use Mahaul,
    DEPLOYMENT_ENV: [
      type: :enum,
      defaults: [dev: "dev", test: "dev"],
      choices: [:dev, :staging, :live]
    ],
    PORT: [type: :port, defaults: [dev: "4000"]],
    DATABASE_URL: [type: :uri, defaults: [dev: "postgresql://user:pass@localhost:5432/app_dev"]],
    ANOTHER_ENV: [type: :host, default: "//localhost"]
end
```

#### Use in `config/runtime.exs`

```elixir
import Config

if config_env() == :dev do
  MyApp.Env.validate()

  config :my_app, MyApp.Endpoint,
    http: [port: MyApp.Env.port()]
end

if config_env() == :prod do
  MyApp.Env.validate!()

  config :my_app, MyApp.Repo,
    url: MyApp.Env.database_url()

  config :my_app, :something,
    another: MyApp.Env.another_env()

  if MyApp.Env.deployment_env() == :staging do
    # configure something more
  end
end
```

#### Use anywhere in the code

```elixir
defmodule MyApp.Magic
  def do_magic(env \\ MyApp.Env.deployment_env())
  def do_magic(:live), do: "Magic on live"
  def do_magic(:staging), do: "Magic on staging"
  def do_magic(:dev), do: "Magic on dev"
end
```

## Supported types

The following type configurations are supported.

| Type    | Elixir Type | Valid Environment variables value                               |
| :------ | :---------- | :-------------------------------------------------------------- |
| `:str`  | String      | Any string                                                      |
| `:enum` | Atom        | Any string                                                      |
| `:num`  | Float       | Any string that can be parsed as float                          |
| `:int`  | Integer     | Any string that can be parsed as integer                        |
| `:bool` | Boolean     | "true" and "1" as `true`; "false" and "0" as `false`            |
| `:port` | Integer     | Any valid port value between "1" to "65535" (casted as integer) |
| `:host` | String      | Any valid host name                                             |
| `:uri`  | String      | Any valid uris                                                  |


## Setting Defaults

Any defaults and fallback values can be set globally using the `default` or for any mix environment using the `defaults` configuration options. Make sure to **use the string values** same as we set in the actual system environment, as it will be parsed depending upon the provided `type` configuration.

#### Globally

```elixir
defmodule MyApp.Env do
  use Mahaul,
    MY_ENV: [type: :str, default: "Hello World"]
end
```

```
iex -S mix
iex> MyApp.Env.my_env()
Hello World

MY_ENV="Hello Universe" iex -S mix
iex> MyApp.Env.my_env()
Hello Universe
```

#### For any mix environment

```elixir
defmodule MyApp.Env do
  use Mahaul,
    MY_ENV: [
      type: :str,
      defaults: [prod: "Hello Prod", dev: "Hello Dev", test: "Hello Test", custom: "Hello Custom"]
    ]
end
```

```
MIX_ENV=prod iex -S mix
iex> MyApp.Env.my_env()
Hello Prod

MIX_ENV=dev iex -S mix
iex> MyApp.Env.my_env()
Hello Dev

MIX_ENV=test iex -S mix
iex> MyApp.Env.my_env()
Hello Test

MIX_ENV=custom iex -S mix
iex> MyApp.Env.my_env()
Hello Custom

MIX_ENV=prod MY_ENV="Hello World" iex -S mix
iex> MyApp.Env.my_env()
Hello World
```

#### For any mix environment with fallback

```elixir
defmodule MyApp.Env do
  use Mahaul,
    MY_ENV: [
      type: :str,
      default: "Hello World",
      defaults: [prod: "Hello Prod", dev: "Hello Dev", test: "Hello Test"]
    ]
end
```

```
MIX_ENV=prod iex -S mix
iex> MyApp.Env.my_env()
Hello Prod

MIX_ENV=dev iex -S mix
iex> MyApp.Env.my_env()
Hello Dev

MIX_ENV=test iex -S mix
iex> MyApp.Env.my_env()
Hello Test

MIX_ENV=custom iex -S mix
iex> MyApp.Env.my_env()
Hello World

MIX_ENV=custom MY_ENV="Hello Universe" iex -S mix
iex> MyApp.Env.my_env()
Hello Universe
```

## Setting choices list

You can further restrict the parsed values to a predefined list by setting the `choices` option with list of allowed values. Note that the values are parsed first and then matched against the provided list.

```elixir
defmodule MyApp.Env do
  use Mahaul,
    DEPLOYMENT_ENV: [type: :enum, choices: [:dev, :staging, :live]],
    DAY_OF_WEEK: [type: :int, choices: [1, 2, 3, 4, 5, 6, 7]]
end
```

## Setting documentation

There is a default documentation added for each of the compile time generated function equivalents for the environment variables. However you may use the `doc` option to add a custom documentation with more details and explanations as per your needs.

#### Default

```elixir
defmodule MyApp.Env do
  use Mahaul,
    PORT: [type: :port, defaults: [dev: "4000"]]
end
```
![Documentation-default](assets/images/doc-default.png)

#### Custom

```elixir
defmodule MyApp.Env do
  use Mahaul,
    PORT: [
      type: :port,
      defaults: [dev: "4000"],
      doc: ~s"""
      This is an example documentation for the environment variable.
      We can use all the features from the elixir `@doc` tag that will
      appear as the function documentation during its usage.

      ## Usage

          Env.port()

      ## Notes
      These are some notes to keep in mind.
      """
    ]
end
```
![Documentation-custom](assets/images/doc-custom.png)

## Why this package

`mahaul` accomplishes the following functionalities for streamlining the environment variables requirements for an elixir app.

#### Compile time access guarantees

Using the meta programming capabilities of Elixir, `mahaul` creates compile time methods for accessing the environment variables. This guarantees that there are no accidental typos during the access of the environment variables from the code. Also as an added bonus, if you are using the [Elixir Language Server](https://github.com/elixir-lsp/elixir-ls) for your development environment, you'd get code autocompletion.

#### Parsed values with accurate data types

Depending upon the configuration, the access to the predefined environment variables string values are parsed and the correct elixir data types are returned. `mahaul` supports [a wide range](#supported-types) of commonly set environment variable types. It also supports the `choices` options to limit the allowed values for an environment variable.

#### Validation of required values before app boot

Often times we release new versions of the app accessing new environment variables, but we forget to set those for one of our app deployments. This creates nasty bugs that are only discovered when certain parts of the app behaves erratically or fails. With `mahaul`, you can pre-validate the existence of the required environment variables with correct values before booting the app (ideally in `config/runtime.exs`). This ensures that your application will fail to boot unless you have set those environment variables with correct values. This works really well with any cloud deployments that makes new version of your app active and available only after ensuring the new deployment had a successful boot.

#### Defaults and fallbacks

You can [set default values](#setting-defaults) for the production or development environment of your app while configuring `mahaul`. This comes handy when you want some defaults for dev/test environment to let other contributors of your app quickly start the dev environment of your app without worrying to set some needed environment variables. Or have some sensible defaults for production version of your app with flexibility to change the values by setting an environment variable.

## Contributing

Contributions are welcome. Please follow the commit guidelines from https://www.conventionalcommits.org.

### Setup

Clone the repo and fetch its dependencies:

```sh
git clone https://github.com/emadalam/mahaul.git
cd mahaul
mix setup
```

### Running tests

```sh
mix test

# or with coverage threshold
# mix coveralls
```

### Building docs

```sh
MIX_ENV=docs mix docs

# or view the docs
# MIX_ENV=docs mix docs --open
```

## LICENSE

See [LICENSE](https://github.com/emadalam/mahaul/blob/main/LICENSE.txt)