Packages

Easy configurations from environment variables and .env files.

Current section

Files

Jump to
dotenv_conf src dotenv_conf.gleam
Raw

src/dotenv_conf.gleam

//// The `dotenv_conf` is made to support both configurations via environment
//// variables (via `envoy`) and .env files.
////
//// .env files are read using `read_file`, then passed to the specific `read_`
//// functions, to avoid reading the file multiple times.
import envoy
import gleam/dict.{type Dict}
import gleam/float
import gleam/int
import gleam/string
import simplifile
/// `EnvFile` represents the environment file result.
/// When the file is read correctly, it contains `Ok(Dict(String, String))`
/// with all the parsed values. If an error occurs, it contains
/// `Error(EnvFileError)`.
///
pub type EnvFile =
Result(Dict(String, String), EnvFileError)
/// `EnvFileError` is an enumeration of the possible errors when dealing with
/// the .env file.
///
/// - `MissingFile`: the file is not found at the provided path.
/// - `InvalidFile`: the file is not correct (likely is a directory or special).
/// - `FileError`: an error occured when reading the file.
/// - `BadConfig`: the format of the file is not correct.
/// - `MissingVar`: the config variable does not exist.
///
pub type EnvFileError {
MissingFile
InvalidFile
FileError
BadConfig
MissingVar
}
/// Reads the config string with the given `name`. If it is not found in the
/// environment variables, the EnvFile read using `read_file` is used.
/// If there was an error reading the file or the key is not found in the file
/// either, an `Error(EnvFileError)` is returned.
///
/// This function can be used to implement custom config types, otherwise
/// `read_<type>_or` is preferrable.
///
pub fn read_string(
name: String,
from_file: EnvFile,
) -> Result(String, EnvFileError) {
case envoy.get(name), from_file {
Ok(value), _ -> Ok(value)
_, Error(err) -> Error(err)
_, Ok(values) ->
case dict.get(values, name) {
Ok(value) -> Ok(value)
_ -> Error(MissingVar)
}
}
}
/// Reads the config string with the given `name`, first from the environment
/// variables and, if not found, from the EnvFile read using `read_file`. If
/// neither works, the given `default` value is used.
///
pub fn read_string_or(
name: String,
from_file: EnvFile,
default: String,
) -> String {
case read_string(name, from_file) {
Ok(value) -> value
_ -> default
}
}
/// Reads the config integer with the given `name`, first from the environment
/// variables and, if not found, from the EnvFile read using `read_file`. If
/// neither works, the given `default` value is used.
///
pub fn read_int_or(name: String, from_file: EnvFile, default: Int) -> Int {
case read_string(name, from_file) {
Error(_) -> default
Ok(value_str) ->
case int.parse(value_str) {
Ok(value) -> value
_ -> default
}
}
}
/// Reads the config float with the given `name`, first from the environment
/// variables and, if not found, from the EnvFile read using `read_file`. If
/// neither works, the given `default` value is used.
///
pub fn read_float_or(name: String, from_file: EnvFile, default: Float) -> Float {
case read_string(name, from_file) {
Error(_) -> default
Ok(value_str) ->
case float.parse(value_str) {
Ok(value) -> value
_ -> default
}
}
}
/// Reads the .env file at the given `path`. It is meant to be used as a use
/// expression.
///
/// ## Example
///
/// ```gleam
/// use file <- read_file(".env")
/// let username = read_string_or("USER", file, "admin")
/// ```
///
pub fn read_file(path: String, then: fn(EnvFile) -> a) -> a {
let file = case simplifile.is_file(path) {
Error(_) -> Error(MissingFile)
Ok(False) -> Error(InvalidFile)
_ ->
case simplifile.read(path) {
Ok(content) -> parse_dotenv_file(content)
Error(_) -> Error(FileError)
}
}
then(file)
}
fn parse_dotenv_file(content: String) -> EnvFile {
content
|> string.trim
|> string.replace("\r\n", "\n")
|> string.split("\n")
|> parse_config_lines([])
}
fn parse_config_lines(
lines: List(String),
acc: List(#(String, String)),
) -> EnvFile {
case lines {
[] -> Ok(dict.from_list(acc))
[line, ..rest] ->
case string.split_once(line, "=") {
Error(Nil) -> Error(BadConfig)
Ok(l) -> parse_config_lines(rest, [l, ..acc])
}
}
}