Packages

Type-safe environment variables for Gleam. Zero dependencies.

Current section

Files

Jump to
envie src envie.gleam
Raw

src/envie.gleam

import envie/decode.{type Decoder}
import envie/error.{type Error, type LoadError}
import envie/parse
import gleam/dict.{type Dict}
import gleam/float
import gleam/int
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
import gleam/uri.{type Uri}
// === FFI declarations ======================================================
@external(erlang, "envie_ffi", "get_env")
@external(javascript, "./envie_ffi.mjs", "get_env")
fn do_get(name: String) -> Result(String, Nil)
@external(erlang, "envie_ffi", "set_env")
@external(javascript, "./envie_ffi.mjs", "set_env")
fn do_set(name: String, value: String) -> Nil
@external(erlang, "envie_ffi", "unset_env")
@external(javascript, "./envie_ffi.mjs", "unset_env")
fn do_unset(name: String) -> Nil
@target(erlang)
@external(erlang, "envie_ffi", "all_env")
fn do_all() -> Dict(String, String)
@target(javascript)
fn do_all() -> Dict(String, String) {
do_all_js() |> dict.from_list()
}
@target(javascript)
@external(javascript, "./envie_ffi.mjs", "all_env")
fn do_all_js() -> List(#(String, String))
@external(erlang, "envie_ffi", "read_file")
@external(javascript, "./envie_ffi.mjs", "read_file")
fn do_read_file(path: String) -> Result(String, Nil)
// === Core API ==============================================================
/// Return the variable's value as `Ok(value)`, or `Error(Nil)` if missing.
pub fn get(name: String) -> Result(String, Nil) {
do_get(name)
}
/// Set an environment variable.
pub fn set(name: String, value: String) -> Nil {
do_set(name, value)
}
/// Remove an environment variable, if present.
pub fn unset(name: String) -> Nil {
do_unset(name)
}
/// Return all environment variables as a `Dict(String, String)`.
pub fn all() -> Dict(String, String) {
do_all()
}
// === Type-safe getters =====================================================
/// Get a string variable, fall back to `default` when missing.
pub fn get_string(name: String, default: String) -> String {
get(name) |> result.unwrap(default)
}
/// Get an integer variable; return `default` if missing or invalid.
pub fn get_int(name: String, default: Int) -> Int {
case get(name) {
Ok(value) -> int.parse(value) |> result.unwrap(default)
Error(_) -> default
}
}
/// Get a float variable; return `default` if missing or invalid.
pub fn get_float(name: String, default: Float) -> Float {
case get(name) {
Ok(value) -> float.parse(value) |> result.unwrap(default)
Error(_) -> default
}
}
/// Get a boolean variable; returns `default` if missing or unrecognised.
/// Accepted truthy: `true`, `yes`, `1`, `on`. Falsy: `false`, `no`, `0`, `off`.
pub fn get_bool(name: String, default: Bool) -> Bool {
case get(name) {
Ok(value) -> {
let decode.Decoder(run) = decode.bool()
run(value) |> result.unwrap(default)
}
Error(_) -> default
}
}
/// Split a variable by `separator` into trimmed, non-empty strings.
pub fn get_string_list(
name: String,
separator separator: String,
default default: List(String),
) -> List(String) {
case get(name) {
Ok(value) -> {
value
|> string.split(separator)
|> list.map(string.trim)
|> list.filter(fn(s) { s != "" })
}
Error(_) -> default
}
}
// === Decoder-based API =====================================================
/// Require a variable and decode it with `decoder`.
/// Returns `Ok(value)` or an `Error` describing missing/invalid input.
pub fn require(name: String, decoder: Decoder(a)) -> Result(a, Error) {
use value <- result.try(
get(name)
|> result.replace_error(error.Missing(name)),
)
let decode.Decoder(run) = decoder
run(value)
|> result.map_error(fn(reason) {
error.InvalidValue(name: name, value: value, reason: reason)
})
}
/// Require a string variable.
pub fn require_string(name: String) -> Result(String, Error) {
require(name, decode.string())
}
/// Require an integer variable.
pub fn require_int(name: String) -> Result(Int, Error) {
require(name, decode.int())
}
/// Require an integer within `[min, max]`.
pub fn require_int_range(
name: String,
min min: Int,
max max: Int,
) -> Result(Int, Error) {
require(name, decode.int_range(min: min, max: max))
}
/// Require a valid URL (permissive).
pub fn require_url(name: String) -> Result(Uri, Error) {
require(name, decode.url())
}
/// Require a URL that has one of the given `schemes`.
pub fn require_url_with_scheme(
name: String,
schemes: List(String),
) -> Result(Uri, Error) {
require(name, decode.url_with_scheme(schemes))
}
/// Shortcut for `require_url_with_scheme(name, ["http", "https"])`.
pub fn require_web_url(name: String) -> Result(Uri, Error) {
require(name, decode.web_url())
}
/// Require a non-empty (trimmed) string.
pub fn require_non_empty_string(name: String) -> Result(String, Error) {
require(name, decode.non_empty_string())
}
/// Require a string that starts with `prefix`.
pub fn require_string_prefix(
name: String,
prefix prefix: String,
) -> Result(String, Error) {
require(name, decode.string_prefix(prefix))
}
/// Require a list of strings split by `separator`.
pub fn require_string_list(
name: String,
separator separator: String,
) -> Result(List(String), Error) {
require(name, decode.string_list(separator: separator))
}
/// Require a list of integers split by `separator`.
pub fn require_int_list(
name: String,
separator separator: String,
) -> Result(List(Int), Error) {
require(name, decode.int_list(separator: separator))
}
/// Require a float variable.
pub fn require_float(name: String) -> Result(Float, Error) {
require(name, decode.float())
}
/// Require a float within `[min, max]`.
pub fn require_float_range(
name: String,
min min: Float,
max max: Float,
) -> Result(Float, Error) {
require(name, decode.float_range(min: min, max: max))
}
/// Require a boolean variable. Accepts the usual true/false aliases.
pub fn require_bool(name: String) -> Result(Bool, Error) {
require(name, decode.bool())
}
/// Require a TCP/UDP port number (1–65535).
pub fn require_port(name: String) -> Result(Int, Error) {
require(name, decode.port())
}
/// Require that a variable equals one of the `allowed` strings.
pub fn require_one_of(
name: String,
allowed: List(String),
) -> Result(String, Error) {
require(name, decode.one_of(allowed))
}
/// Decode an optional variable: `Ok(None)` if missing, `Ok(Some(v))` if valid, otherwise `Error`.
pub fn optional(name: String, decoder: Decoder(a)) -> Result(Option(a), Error) {
case get(name) {
Error(Nil) -> Ok(None)
Ok(value) -> {
let decode.Decoder(run) = decoder
run(value)
|> result.map(Some)
|> result.map_error(fn(reason) {
error.InvalidValue(name: name, value: value, reason: reason)
})
}
}
}
// === .env file loading =====================================================
/// Load variables from `.env` in the current directory; does not overwrite existing vars.
pub fn load() -> Result(Nil, LoadError) {
load_from(".env")
}
/// Load variables from `path`; existing environment variables are preserved.
pub fn load_from(path: String) -> Result(Nil, LoadError) {
do_load_from(path, False)
}
/// Like `load`, but overwrite existing variables with `.env` values.
pub fn load_override() -> Result(Nil, LoadError) {
load_override_from(".env")
}
/// Load from `path`, overwriting existing variables.
pub fn load_override_from(path: String) -> Result(Nil, LoadError) {
do_load_from(path, True)
}
/// Load variables from `.env` formatted `content`; does not overwrite by default.
pub fn load_from_string(content: String) -> Result(Nil, LoadError) {
do_load_from_string(content, False)
}
/// Load variables from `content`, overwriting existing variables.
pub fn load_from_string_override(content: String) -> Result(Nil, LoadError) {
do_load_from_string(content, True)
}
fn do_load_from(path: String, overwrite: Bool) -> Result(Nil, LoadError) {
use content <- result.try(
do_read_file(path)
|> result.replace_error(error.FileNotFound(path)),
)
do_load_from_string(content, overwrite)
}
fn do_load_from_string(
content: String,
overwrite: Bool,
) -> Result(Nil, LoadError) {
use vars <- result.try(
parse.parse_dotenv(content)
|> result.map_error(error.ParseError),
)
list.each(vars, fn(pair) {
let #(key, value) = pair
case overwrite {
True -> set(key, value)
False -> {
case get(key) {
Ok(_) -> Nil
Error(_) -> set(key, value)
}
}
}
})
Ok(Nil)
}
/// Parse `.env` content into key/value pairs; returns parse errors if any.
pub fn parse_dotenv(
content: String,
) -> Result(List(#(String, String)), List(error.ParseError)) {
parse.parse_dotenv(content)
}
// === Error formatting ======================================================
/// Format a single `Error` for display.
pub fn format_error(err: Error) -> String {
error.format(err)
}
/// Format multiple `Error` values, one per line.
pub fn format_errors(errors: List(Error)) -> String {
error.format_all(errors)
}
/// Format a `LoadError` for display.
pub fn format_load_error(err: LoadError) -> String {
error.format_load_error(err)
}