Packages
enux
1.2.2
1.6.0
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.3
1.1.2
1.1.1
1.1.0
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
utility package for loading, validating and documenting your app's configuration variables from env, json, jsonc and toml files at runtime and injecting them into your environment
Current section
Files
Jump to
Current section
Files
lib/enux.ex
defmodule Enux do
@moduledoc """
utility package for loading, validating and documenting your app's configuration variables from env, json, jsonc and toml files at runtime and injecting them into your environment
## Installation
```
defp deps do
[
{:enux, "~> 1.2.2"},
# if you want to load `.jsonc` files, you should have this
# you can also use this for `.json` files
{:jsonc, "~> 0.4"},
# if you want to load `.json` files, you should have either this
{:json, "~> 1.4"}
# or this
{:jason, "~> 1.3"}
# or this
{:jaxon, "~> 2.0"}
# or this
{:thoas, "~> 0.2"}
# or this
{:jsone, "~> 1.7"}
# or this
{:jiffy, "~> 1.1"}
# or this
{:poison, "~> 5.0"}
# if you want to load `.toml` files, you should have either this
{:toml, "~> 0.6"}
# or this
{:tomerl, "~> 0.5"}
# or this
{:tomlex, "~> 0.0"}
]
end
```
## Usage
In elixir 1.11, `config/runtime.exs` was introduced. This is a file that is executed exactly before your application starts.
This is a proper place to load any configuration variables into your app. If this file does not exist in your project directory,
create it and add these lines to it:
```
import Config
env = Enux.load()
config :otp_app, env
```
When you start your application, you can access your configuration variables using `Applicatoin.get_env`.
If you need to url encode your configuration values, just pass `url_encoded: true` to `Enux.load`.
You should have either [poison](https://hex.pm/packages/poison) or [jason](https://hex.pm/packages/jason) or [jaxon](https://hex.pm/packages/jaxon)
or [thoas](https://hex.pm/packages/thoas) or [jsone](https://hex.pm/packages/jsone) or [jiffy](https://hex.pm/packages/jiffy) or [json](https://hex.pm/packages/json)
in your dependencies if you want to use `.json` files.
To use `.jsonc` files, you should have [jsonc](https://hex.pm/packages/jsonc). You can also use this package for `.json` files.
To use `.toml` files, you should have either [toml](https://hex.pm/packages/toml) or [tomerl](https://hex.pm/packages/tomerl) or [tomlex](https://hex.pm/packages/tomlex).
You can load multiple files of different kinds:
```
import Config
env1 = Enux.load("config/one.env", url_encoded: true)
config :otp_app, env1
env2 = Enux.load("config/two.json")
config :otp_app, :two, env2
```
Another way of using Enux is using the `Enux.autoload` function which will load all `.env`, `.json`, `.jsonc` and `.toml` files in your `config` directory.
it makes more sense to call this function in your `config/runtime.exs` but you can call it anywhere in your code.
If you have `config/pg.env` and `config/redis.json` in your project directory, after calling `Enux.autoload(:otp_app)`, you can access the variables
using `Application.get_env(:otp_app, :pg)` and `Application.get_env(:otp_app, :redis)`. if a file is named `.env` or `.json` or `.jsonc` or `.toml`, you should use
`Application.get_env(:otp_app, :env)` or `Application.get_env(:otp_app, :json)` or `Application.get_env(:otp_app, :jsonc)` or `Application.get_env(:otp_app, :toml)` respectively.
```
Enux.autoload(:otp_app)
```
You may also use `Enux.expect` to both validate and document your required environment. first you need to define a schema:
```
schema = [
id: [&is_integer/1, fn id -> id > 1000 end],
username: [&is_binary/1, fn u -> String.length(u) > 8 end],
metadata: [],
profile: [
full_name: [&is_binary/1],
age: [&is_number/1]
]
]
```
then the following line will check for compliance of your environment under `:otp_app` and `:key` with the schema defined above
(an empty list implies only checking for existence):
```
Enux.expect(:otp_app, :key, schema)
```
"""
alias Enux.Env
alias Enux.Json
alias Enux.Jsonc
alias Enux.Toml
@doc """
reads the variables in `config/.env` and returns a formatted keyword list.
all values are loaded as they are.
"""
def load() do
File.stream!("config/.env", [], :line) |> Env.decode([])
end
@doc """
reads the variables in `config/.env` and returns a formatted keyword list
"""
def load(opts) when is_list(opts) do
File.stream!("config/.env", [], :line) |> Env.decode(opts)
end
@doc """
reads the variables in the given path(could be `.env`, `.json`, `.jsonc` or `.toml` file) and returns a formatted keyword list
"""
def load(path, opts \\ []) when is_binary(path) and is_list(opts) do
case String.split(path, ".") |> Enum.at(1) |> String.to_atom() do
:env -> File.stream!(path, [], :line) |> Env.decode(opts)
:json -> File.read!(path) |> Json.decode(opts)
:jsonc -> File.read!(path) |> Jsonc.decode(opts)
:toml -> File.read!(path) |> Toml.decode(opts)
ext -> raise "unsupported file type: #{ext}"
end
end
@doc """
automatically loads all `.env`, `.json`, `.jsonc` and `.toml` files in your `config` directory.
pass your project's name as an atom. you can also still pass `url_encoded: true` to it.
"""
def autoload(app, opts \\ []) when is_atom(app) and is_list(opts) do
files =
File.ls!("config")
|> Enum.map(fn f -> f |> String.split(".") end)
|> Enum.filter(fn [_, ext] -> ext in ["env", "json", "jsonc", "toml"] end)
|> Enum.map(fn f -> Enum.join(f, ".") end)
cond do
Enum.empty?(files) ->
raise "There is no `.env`, `.json`, `.jsonc` or `.toml` file in your config directory"
true ->
files
|> Enum.map(fn f -> [f, Enux.load("config/#{f}", opts)] end)
|> Enum.each(fn [f, kwl] ->
key =
case f do
".env" ->
:env
".json" ->
:json
".jsonc" ->
:jsonc
".toml" ->
:toml
_ ->
String.split(f, ".") |> Enum.at(0) |> String.to_atom()
end
Application.put_env(app, key, kwl)
end)
end
end
@doc """
checks if the environment variables under `app` and `key` comply with the given `schema`. any non-compliance results in an error.
you can use this function for both validating and documenting your required environment.
"""
def expect(app, key, schema) when is_atom(app) and is_atom(key) and is_list(schema) do
case Application.get_env(app, key) do
nil ->
raise "environment with key #{key} does not exist"
env ->
cond do
Keyword.keyword?(env) ->
if !Keyword.keyword?(schema) do
raise "schema should be a keyword list"
end
check(env, schema)
true ->
check_item(env, schema, [])
end
end
end
defp check(env, schema, parents \\ [])
when is_list(env) and is_list(schema) and is_list(parents) do
schema
|> Enum.each(fn {key, sub_schema} ->
case env |> Keyword.get(key) do
nil ->
raise "your environment should contain #{parents |> Enum.reverse() |> Enum.join(".")}.#{key}"
value ->
cond do
Keyword.keyword?(value) ->
check(value, sub_schema, [key | parents])
true ->
check_item(value, sub_schema, [key | parents])
end
end
end)
end
defp check_item(value, conditions, parents)
when is_list(conditions) and is_list(parents) do
conditions
|> Enum.each(fn c ->
case check_item(value, c) do
false ->
raise "condition #{inspect(c)} was not met for #{parents |> Enum.reverse() |> Enum.join(".")}"
true ->
nil
end
end)
end
defp check_item(value, condition) when is_function(condition) do
case condition.(value) do
result when is_boolean(result) ->
result
_ ->
raise "function #{inspect(condition)} does not return a boolean"
end
end
end