README.md

<p align="center">
  <img src="https://raw.githubusercontent.com/lupodevelop/envie/91a8dd6/assets/img/envie.png" alt="envie logo" width="200" />
</p>

[![Package Version](https://img.shields.io/hexpm/v/envie)](https://hex.pm/packages/envie)[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/envie/)[![Built with Gleam](https://img.shields.io/badge/built%20with-gleam-ffaff3?logo=gleam)](https://gleam.run)[![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

# envie

> Why the name? `envy` was already taken on Hex, so we went with `envie`. 
> Because you shouldn't be jealous of other languages' config loaders β€” you should just *desire* (French: *envie*) a better one. Plus, it makes your environment variables feel 20% more sophisticated. πŸ₯

Type-safe environment configuration for Gleam.
Cross-platform and zero runtime dependencies.

envie is simple at first, and scales cleanly as your application grows.
From single variables to fully structured configuration.

## What envie helps you do

- read and write env vars without ceremony
- parse values into real types
- keep configuration explicit and predictable
- scale from simple defaults to structured config
- inspect what actually happened at runtime

## Quick start

```sh
gleam add envie
```

```gleam
import envie

pub fn main() {
  let port = envie.get_int("PORT", 3000)
  let debug = envie.get_bool("DEBUG", False)

  // That's it. No setup, no ceremony.
}
```

## Core API

Read, write, and remove environment variables on any target.

```gleam
envie.get("HOME")           // -> Ok("/Users/you")
envie.set("APP_ENV", "production")
envie.unset("TEMP_TOKEN")
let all = envie.all()       // -> Dict(String, String)
```

## Type-safe getters

Parse common types with defaults.

```gleam
let port    = envie.get_int("PORT", 3000)
let ratio   = envie.get_float("RATIO", 1.0)
let debug   = envie.get_bool("DEBUG", False)
let origins = envie.get_string_list("ORIGINS", separator: ",", default: [])
```

Boolean parsing accepts:
- `true`, `yes`, `1`, `on`
- `false`, `no`, `0`, `off`

## Validated access

Use `require_*` when missing or invalid values should fail.

```gleam
import envie

let assert Ok(key)  = envie.require_string("API_KEY")
let assert Ok(port) = envie.require_port("PORT")
let assert Ok(url)  = envie.require_web_url("DATABASE_URL")
let assert Ok(name) = envie.require_non_empty_string("APP_NAME")
let assert Ok(on)   = envie.require_bool("DEBUG")
let assert Ok(ratio) = envie.require_float_range("RATIO", min: 0.0, max: 1.0)
let assert Ok(env)  = envie.require_one_of("APP_ENV", ["development", "staging", "production"])
```

## Custom validation

Compose a decoder with `envie/decode` when built-in checks are not enough.

```gleam
import envie
import envie/decode
import gleam/string

let secret_decoder =
  decode.string()
  |> decode.validated(fn(s) {
    case string.length(s) >= 32 {
      True -> Ok(s)
      False -> Error("Secret must be at least 32 characters")
    }
  })

let assert Ok(secret) = envie.require("JWT_SECRET", secret_decoder)
```

## Optional variables

```gleam
let assert Ok(maybe_port) = envie.optional("METRICS_PORT", decode.int())
// Ok(None) when missing, Ok(Some(value)) when present and valid
```

## `.env` file loading

Load `.env` files with comments, quoted values, and `export` prefixes.
By default, existing environment values are preserved.
Use `load_override` / `load_override_from` to overwrite.

```gleam
let assert Ok(Nil) = envie.load()
let assert Ok(Nil) = envie.load_from("config/.env.local")
let assert Ok(Nil) = envie.load_from_string("PORT=8080")

let assert Ok(Nil) = envie.load_override()
```

Example `.env` file:

```env
# Application
PORT=8080
DEBUG=true

# Secrets
export API_KEY="sk-1234567890"
DB_PASSWORD='hunter2'
```

## When you need more than defaults

As your app grows, configuration tends to spread across modules and become harder to validate and reason about.

`envie/schema` keeps everything in one place.

```gleam
import envie/schema
import envie/decode

pub type Config {
  Config(
    port: Int,
    debug: Bool,
  )
}

let config_schema =
  schema.build2(
    schema.field("PORT", decode.int())
    |> schema.default(3000),

    schema.field("DEBUG", decode.bool())
    |> schema.default(False),

    Config,
  )

let assert Ok(config) = schema.load(config_schema)
```

This gives you:
- one place for config declarations
- typed parsing and validation
- explicit defaults and optional values

## Multiple environments

For more complex setups (multiple environments, overrides, deployments), you can load multiple `.env` files with explicit priority.

```gleam
import envie/dotenv

let assert Ok(Nil) =
  dotenv.load([
    dotenv.override(".env"),
    dotenv.optional(".env.local"),
    dotenv.required(".env.production"),
  ])
```

- `override` overwrites existing values
- `optional` is ignored if missing
- `required` fails if missing

## Debugging configuration

When configuration becomes non-trivial, it’s often unclear:
- which values were actually read
- which defaults were applied
- where a failure happened

`envie/inspect` lets you trace configuration loading:

```gleam
import envie/inspect

let trace = inspect.load_with_trace(config_schema)
```

This is especially useful in debugging production issues or validating deployments.

## Testing utilities

Helpers that guarantee the environment is restored after each test.

```gleam
import envie
import envie/testing

pub fn my_feature_test() {
  testing.with_env([#("PORT", "3000"), #("DEBUG", "true")], fn() {
    let port = envie.get_int("PORT", 8080)
    port |> should.equal(3000)
  })
  // Original environment is restored automatically
}

pub fn isolated_test() {
  testing.isolated(fn() {
    envie.get("PATH") |> should.equal(Error(Nil))
  })
  // Everything restored
}
```

> [!WARNING]
> **Test Concurrency**: Environment variables are global to the process. Since Gleam runs tests in parallel by default, multiple tests using `testing.*` concurrently may interfere with each other. Use `gleam test -- --seed 123` (or any seed) to run tests with a single worker if you encounter flaky tests.

## Error formatting

Every error type carries enough context to produce clear messages.

```gleam
case envie.require_int("PORT") {
  Ok(port) -> start_server(port)
  Error(err) -> {
    io.println_error(envie.format_error(err))
    // "PORT: invalid value "abc" β€” Expected integer, got: abc"
  }
}
```

## API at a glance

| Function                             | Returns                    | Notes                |
|--------------------------------------|----------------------------|----------------------|
| `get`                                | `Result(String, Nil)`      | Raw access           |
| `set`                                | `Nil`                      |                      |
| `unset`                              | `Nil`                      |                      |
| `all`                                | `Dict(String, String)`     |                      |
| `get_string`                         | `String`                   | Falls back to caller-supplied default |
| `get_int`                            | `Int`                      | Falls back to caller-supplied default |
| `get_float`                          | `Float`                    | Falls back to caller-supplied default |
| `get_bool`                           | `Bool`                     | Falls back to default; true/yes/1/on  |
| `get_string_list`                    | `List(String)`             | Falls back to default; splits & trims |
| `require`                            | `Result(a, Error)`         | Decoder-based        |
| `require_string`                     | `Result(String, Error)`    |                      |
| `require_int`                        | `Result(Int, Error)`       |                      |
| `require_int_range`                  | `Result(Int, Error)`       |                      |
| `require_float`                      | `Result(Float, Error)`     |                      |
| `require_float_range`                | `Result(Float, Error)`     |                      |
| `require_url`                        | `Result(Uri, Error)`       | Permissive RFC parse |
| `require_url_with_scheme`            | `Result(Uri, Error)`       | e.g. ["postgres"]    |
| `require_web_url`                    | `Result(Uri, Error)`       | http or https only   |
| `require_non_empty_string`           | `Result(String, Error)`    |                      |
| `require_string_prefix`              | `Result(String, Error)`    |                      |
| `require_string_list`                | `Result(List(String), Error)` |                   |
| `require_int_list`                   | `Result(List(Int), Error)` |                      |
| `require_bool`                       | `Result(Bool, Error)`      | true/yes/1/on        |
| `require_port`                       | `Result(Int, Error)`       | 1–65 535             |
| `require_one_of`                     | `Result(String, Error)`    | Allow-list check     |
| `optional`                           | `Result(Option(a), Error)` |                      |
| `load`                               | `Result(Nil, LoadError)`   | `.env` in cwd        |
| `load_from`                          | `Result(Nil, LoadError)`   | Custom path          |
| `load_from_string`                   | `Result(Nil, LoadError)`   | From string          |
| `load_override`                      | `Result(Nil, LoadError)`   | Overwrites env       |
| `load_override_from`                 | `Result(Nil, LoadError)`   | Overwrites env       |
| `load_from_string_override`          | `Result(Nil, LoadError)`   | Overwrites env       |

## Cross-platform

---

envie works on Erlang and all major JavaScript runtimes.

- **Erlang/OTP** β€” uses `os:getenv/1`, `os:putenv/2`, `os:unsetenv/1`.
- **JavaScript (Node.js, Bun)** β€” uses `process.env` and `fs` module.
- **JavaScript (Deno)** β€” uses `Deno.env` and `Deno.readTextFileSync`.
- **JavaScript (Browser)** β€” environment variables and `.env` loading are safely disabled (returning `Error`) without crashing your build or runtime.

## Dependencies & Requirements

---

* Gleam **1.14** or newer.
* OTP 27+ on the BEAM.
* Just `gleam_stdlib` β€” no runtime dependencies.

---

<p align="center">Made with Gleam πŸ’œ</p>