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) }