//// 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__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]) } } }