mirror of
https://github.com/darwincereska/envkit.git
synced 2026-06-11 10:23:23 -05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36c74e0c25 | |||
| 56339890e5 | |||
| 5743c0b490 |
Generated
+1
-1
@@ -4,4 +4,4 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "envkit"
|
name = "envkit"
|
||||||
version = "0.1.1"
|
version = "0.2.1"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "envkit"
|
name = "envkit"
|
||||||
version = "0.1.1"
|
version = "0.2.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Darwin Cereska <discorddurr@gmail.com>"]
|
authors = ["Darwin Cereska <discorddurr@gmail.com>"]
|
||||||
description = "A dead-simple env loader"
|
description = "A dead-simple env loader"
|
||||||
|
|||||||
@@ -1,74 +1,174 @@
|
|||||||
# envkit
|
# envkit
|
||||||
|
|
||||||
`envkit` is a small Rust crate for reading environment variables and converting them into typed values.
|
`envkit` is a small Rust crate for reading environment variables as typed values.
|
||||||
|
|
||||||
It keeps the API minimal:
|
It provides a minimal loader API for required values, optional values, defaults,
|
||||||
|
prefixed variable names, and simple `.env`-style files.
|
||||||
|
|
||||||
- `EnvLoader::get` reads a required variable.
|
## Install
|
||||||
- `EnvLoader::get_or` reads a variable with a fallback.
|
|
||||||
- `EnvLoader::get_opt` returns `Option<T>`.
|
|
||||||
- `with_prefix` lets you build loaders for namespaced variables like `APP_PORT` or `DB_HOST`.
|
|
||||||
|
|
||||||
## Supported types
|
Add `envkit` to your `Cargo.toml`:
|
||||||
|
|
||||||
`envkit` currently supports:
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
envkit = "0.2.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use envkit::EnvLoader;
|
||||||
|
|
||||||
|
fn main() -> Result<(), envkit::error::EnvError> {
|
||||||
|
let env = EnvLoader::new();
|
||||||
|
|
||||||
|
let port: u16 = env.get("PORT")?;
|
||||||
|
let debug: bool = env.get_or("DEBUG", false);
|
||||||
|
let app_name: String = env
|
||||||
|
.get_opt("APP_NAME")
|
||||||
|
.unwrap_or_else(|| "envkit".to_string());
|
||||||
|
|
||||||
|
println!("{app_name} listening on port {port}; debug={debug}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### `EnvLoader::get`
|
||||||
|
|
||||||
|
Reads a required environment variable and parses it into the requested type.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let env = envkit::EnvLoader::new();
|
||||||
|
let port: u16 = env.get("PORT")?;
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns `EnvError::MissingVar` when the variable is not set and
|
||||||
|
`EnvError::Invalid` when parsing fails.
|
||||||
|
|
||||||
|
### `EnvLoader::get_or`
|
||||||
|
|
||||||
|
Reads an environment variable and returns a fallback when the variable is
|
||||||
|
missing or invalid.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let debug: bool = env.get_or("DEBUG", false);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `EnvLoader::get_opt`
|
||||||
|
|
||||||
|
Reads an environment variable and returns `None` when the variable is missing or
|
||||||
|
invalid.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let name: Option<String> = env.get_opt("APP_NAME");
|
||||||
|
```
|
||||||
|
|
||||||
|
### `EnvLoader::with_prefix`
|
||||||
|
|
||||||
|
Creates a loader that prepends a prefix to every key.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let app = env.with_prefix("APP_");
|
||||||
|
|
||||||
|
let host: String = app.get("HOST")?; // reads APP_HOST
|
||||||
|
let port: u16 = app.get_or("PORT", 8080); // reads APP_PORT
|
||||||
|
```
|
||||||
|
|
||||||
|
### `EnvLoader::load_file`
|
||||||
|
|
||||||
|
Loads variables from a file with one `KEY=VALUE` pair per line.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let env = envkit::EnvLoader::new();
|
||||||
|
env.load_file(".env")?;
|
||||||
|
```
|
||||||
|
|
||||||
|
Blank lines and lines starting with `#` are ignored. Keys and values are
|
||||||
|
trimmed before being written into the process environment.
|
||||||
|
|
||||||
|
Example file:
|
||||||
|
|
||||||
|
```text
|
||||||
|
# .env
|
||||||
|
PORT=8080
|
||||||
|
DEBUG=true
|
||||||
|
APP_NAME=envkit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Types
|
||||||
|
|
||||||
|
Built-in parsing supports:
|
||||||
|
|
||||||
- `String`
|
- `String`
|
||||||
- `bool`
|
- `bool`
|
||||||
- numeric types: `i8`, `i16`, `i32`, `i64`, `i128`, `isize`, `u8`, `u16`, `u32`, `u64`, `u128`, `usize`, `f32`, `f64`
|
- signed integers: `i8`, `i16`, `i32`, `i64`, `i128`, `isize`
|
||||||
|
- unsigned integers: `u8`, `u16`, `u32`, `u64`, `u128`, `usize`
|
||||||
|
- floats: `f32`, `f64`
|
||||||
|
- `Vec<T>` where `T` also implements `FromEnv`
|
||||||
|
|
||||||
Boolean parsing accepts:
|
Boolean values accept:
|
||||||
|
|
||||||
- `true` / `false`
|
- `true` / `false`
|
||||||
- `1` / `0`
|
- `1` / `0`
|
||||||
- `t` / `f`
|
- `t` / `f`
|
||||||
|
- `yes` / `no`
|
||||||
|
|
||||||
## Quick start
|
Lists are comma-separated and each item is trimmed before parsing:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use envkit::EnvLoader;
|
let env = envkit::EnvLoader::new();
|
||||||
|
let ports: Vec<u16> = env.get("PORTS")?;
|
||||||
fn main() -> Result<(), envkit::error::EnvError> {
|
let flags: Vec<bool> = env.get("FEATURE_FLAGS")?;
|
||||||
let loader = EnvLoader;
|
|
||||||
|
|
||||||
let port: u16 = loader.get("PORT")?;
|
|
||||||
let debug: bool = loader.get_or("DEBUG", false);
|
|
||||||
let app_name: String = loader.get_opt("APP_NAME").unwrap_or_else(|| "envkit".to_string());
|
|
||||||
|
|
||||||
println!("port={port}, debug={debug}, app_name={app_name}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Prefixed variables
|
## Custom Types
|
||||||
|
|
||||||
|
Implement `FromEnv` for your own types when you want domain-specific parsing.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use envkit::EnvLoader;
|
use envkit::{error::EnvError, FromEnv};
|
||||||
|
|
||||||
fn main() -> Result<(), envkit::error::EnvError> {
|
enum LogFormat {
|
||||||
let loader = EnvLoader;
|
Json,
|
||||||
let app = loader.with_prefix("APP_");
|
Pretty,
|
||||||
|
}
|
||||||
|
|
||||||
let host: String = app.get("HOST")?;
|
impl FromEnv for LogFormat {
|
||||||
let port: u16 = app.get_or("PORT", 8080);
|
fn from_env(value: &str) -> Result<Self, EnvError> {
|
||||||
|
match value.trim().to_lowercase().as_str() {
|
||||||
println!("host={host}, port={port}");
|
"json" => Ok(Self::Json),
|
||||||
Ok(())
|
"pretty" => Ok(Self::Pretty),
|
||||||
|
other => Err(EnvError::invalid(
|
||||||
|
"LogFormat",
|
||||||
|
other,
|
||||||
|
"expected `json` or `pretty`",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If you use the prefixed loader above, it reads `APP_HOST` and `APP_PORT`.
|
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
`envkit` returns `EnvError` for two cases:
|
`envkit` returns `EnvError` values:
|
||||||
|
|
||||||
- `MissingVar` when the environment variable is not present
|
- `EnvError::MissingVar(key)` when a required environment variable is not set.
|
||||||
- `Invalid` when parsing fails
|
- `EnvError::Invalid { key, value, message }` when a value cannot be parsed.
|
||||||
|
- `EnvError::FileError { path, message }` when a file cannot be read or contains
|
||||||
|
an invalid line.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
See the `examples/` folder for runnable usage samples.
|
Run the included examples with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --example basic
|
||||||
|
cargo run --example prefixed
|
||||||
|
```
|
||||||
|
|
||||||
|
The examples seed their own environment variables so they can be run directly.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+16
-5
@@ -1,15 +1,26 @@
|
|||||||
use envkit::EnvLoader;
|
use envkit::EnvLoader;
|
||||||
|
|
||||||
fn main() -> Result<(), envkit::error::EnvError> {
|
fn main() -> Result<(), envkit::error::EnvError> {
|
||||||
let loader = EnvLoader;
|
unsafe {
|
||||||
|
std::env::set_var("PORT", "8080");
|
||||||
|
std::env::set_var("DEBUG", "true");
|
||||||
|
std::env::set_var("APP_NAME", "envkit");
|
||||||
|
std::env::set_var("ALLOWED_PORTS", "8080, 8081, 8082");
|
||||||
|
}
|
||||||
|
|
||||||
|
let loader = EnvLoader::new();
|
||||||
|
|
||||||
let port: u16 = loader.get("PORT")?;
|
let port: u16 = loader.get("PORT")?;
|
||||||
let debug: bool = loader.get_or("DEBUG", false);
|
let debug: bool = loader.get_or("DEBUG", false);
|
||||||
let app_name: String = loader.get_opt("APP_NAME").unwrap_or_else(|| "envkit".to_string());
|
let app_name: String = loader
|
||||||
|
.get_opt("APP_NAME")
|
||||||
|
.unwrap_or_else(|| "envkit".to_string());
|
||||||
|
let allowed_ports: Vec<u16> = loader.get("ALLOWED_PORTS")?;
|
||||||
|
|
||||||
println!("port={port}");
|
println!("{app_name}");
|
||||||
println!("debug={debug}");
|
println!("port: {port}");
|
||||||
println!("app_name={app_name}");
|
println!("debug: {debug}");
|
||||||
|
println!("allowed ports: {allowed_ports:?}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -1,16 +1,22 @@
|
|||||||
use envkit::EnvLoader;
|
use envkit::EnvLoader;
|
||||||
|
|
||||||
fn main() -> Result<(), envkit::error::EnvError> {
|
fn main() -> Result<(), envkit::error::EnvError> {
|
||||||
let loader = EnvLoader;
|
unsafe {
|
||||||
|
std::env::set_var("APP_HOST", "127.0.0.1");
|
||||||
|
std::env::set_var("APP_PORT", "3000");
|
||||||
|
std::env::set_var("APP_ENABLED", "yes");
|
||||||
|
}
|
||||||
|
|
||||||
|
let loader = EnvLoader::new();
|
||||||
let app = loader.with_prefix("APP_");
|
let app = loader.with_prefix("APP_");
|
||||||
|
|
||||||
let host: String = app.get("HOST")?;
|
let host: String = app.get("HOST")?;
|
||||||
let port: u16 = app.get_or("PORT", 8080);
|
let port: u16 = app.get_or("PORT", 8080);
|
||||||
let enabled: bool = app.get_or("ENABLED", false);
|
let enabled: bool = app.get_or("ENABLED", false);
|
||||||
|
|
||||||
println!("host={host}");
|
println!("host: {host}");
|
||||||
println!("port={port}");
|
println!("port: {port}");
|
||||||
println!("enabled={enabled}");
|
println!("enabled: {enabled}");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-3
@@ -6,15 +6,18 @@ pub enum EnvError {
|
|||||||
|
|
||||||
/// An environment variable was expected, but it was not found.
|
/// An environment variable was expected, but it was not found.
|
||||||
MissingVar(String),
|
MissingVar(String),
|
||||||
|
|
||||||
|
/// A file was expected to be found, but it was not found or could not be read.
|
||||||
|
FileError { path: String, message: String }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EnvError {
|
impl EnvError {
|
||||||
/// Creates a new `EnvError::Invalid` with the given key, value, and message.
|
/// Creates a new `EnvError::Invalid` with the given key, value, and message.
|
||||||
pub fn invalid<K, V, M>(key: K, value: V, message: M) -> Self
|
pub fn invalid<K, V, M>(key: K, value: V, message: M) -> Self
|
||||||
where
|
where
|
||||||
K: Into<String>,
|
K: Into<String>, // The key of the environment variable that was invalid.
|
||||||
V: Into<String>,
|
V: Into<String>, // The value of the environment variable that was invalid.
|
||||||
M: Into<String>,
|
M: Into<String>, // A message describing why the value was invalid.
|
||||||
{
|
{
|
||||||
Self::Invalid {
|
Self::Invalid {
|
||||||
key: key.into(),
|
key: key.into(),
|
||||||
@@ -28,4 +31,12 @@ impl EnvError {
|
|||||||
where T: Into<String> {
|
where T: Into<String> {
|
||||||
Self::MissingVar(key.into())
|
Self::MissingVar(key.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a new `EnvError::FileMissing` with the given path and message.
|
||||||
|
pub fn file_error<P: Into<String>, M: Into<String>>(path: P, message: M) -> Self {
|
||||||
|
Self::FileError {
|
||||||
|
path: path.into(),
|
||||||
|
message: message.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+39
@@ -28,6 +28,16 @@ impl<T: Number> FromEnv for T {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds support for loading lists of values from environment variables.
|
||||||
|
impl<T: FromEnv> FromEnv for Vec<T> {
|
||||||
|
fn from_env(value: &str) -> Result<Self, EnvError> {
|
||||||
|
match parser::parse_list(value) {
|
||||||
|
Ok(value) => Ok(value),
|
||||||
|
Err(e) => Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Adds support for loading strings from environment variables.
|
/// Adds support for loading strings from environment variables.
|
||||||
impl FromEnv for String {
|
impl FromEnv for String {
|
||||||
fn from_env(value: &str) -> Result<String, EnvError> {
|
fn from_env(value: &str) -> Result<String, EnvError> {
|
||||||
@@ -39,6 +49,35 @@ impl FromEnv for String {
|
|||||||
pub struct EnvLoader;
|
pub struct EnvLoader;
|
||||||
|
|
||||||
impl EnvLoader {
|
impl EnvLoader {
|
||||||
|
/// Creates a new `EnvLoader`.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads environment variables from a file. The file should have one variable per line, in the format `KEY=VALUE`. Lines starting with `#` are treated as comments and ignored.
|
||||||
|
pub fn load_file(&self, path: &str) -> Result<(), EnvError> {
|
||||||
|
// Load the file
|
||||||
|
let contents = std::fs::read_to_string(path).map_err(|e| EnvError::file_error(path, e.to_string()))?;
|
||||||
|
|
||||||
|
// Parse the file
|
||||||
|
for line in contents.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() || line.starts_with("#") { continue }
|
||||||
|
let parts: Vec<&str> = line.splitn(2, '=').collect();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(EnvError::file_error(path, format!("Invalid line: {}", line)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim the key and value
|
||||||
|
let key = parts[0].trim();
|
||||||
|
let value = parts[1].trim();
|
||||||
|
|
||||||
|
// Set the environment variable
|
||||||
|
unsafe { std::env::set_var(key, value) }
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a new `EnvLoader` with the given prefix.
|
/// Creates a new `EnvLoader` with the given prefix.
|
||||||
pub fn with_prefix<P: Into<String>>(&self, prefix: P) -> PrefixedEnvLoader {
|
pub fn with_prefix<P: Into<String>>(&self, prefix: P) -> PrefixedEnvLoader {
|
||||||
PrefixedEnvLoader {
|
PrefixedEnvLoader {
|
||||||
|
|||||||
+30
-9
@@ -1,5 +1,6 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::any::type_name;
|
use std::any::type_name;
|
||||||
|
use crate::FromEnv;
|
||||||
use crate::error::EnvError;
|
use crate::error::EnvError;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
@@ -21,18 +22,24 @@ impl Number for usize {}
|
|||||||
impl Number for f32 {}
|
impl Number for f32 {}
|
||||||
impl Number for f64 {}
|
impl Number for f64 {}
|
||||||
|
|
||||||
|
/// Normalizes a string by trimming whitespace and converting to lowercase.
|
||||||
|
fn normalize(input: &str) -> String {
|
||||||
|
input.trim().to_lowercase().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses a string as a boolean value.
|
/// Parses a string as a boolean value.
|
||||||
pub fn parse_bool(value: &str) -> Result<bool, EnvError> {
|
pub fn parse_bool(value: &str) -> Result<bool, EnvError> {
|
||||||
if value.to_lowercase() == "t" { return Ok(true) }
|
let normalized = normalize(value);
|
||||||
if value.to_lowercase() == "f" { return Ok(false) }
|
if normalized == "t" { return Ok(true) }
|
||||||
if value == "1" { return Ok(true) }
|
if normalized == "f" { return Ok(false) }
|
||||||
if value == "0" { return Ok(false) }
|
if normalized == "1" { return Ok(true) }
|
||||||
if value == "yes" { return Ok(true) }
|
if normalized == "0" { return Ok(false) }
|
||||||
if value == "no" { return Ok(false) }
|
if normalized == "yes" { return Ok(true) }
|
||||||
|
if normalized == "no" { return Ok(false) }
|
||||||
|
|
||||||
match value.parse::<bool>() {
|
match normalized.parse::<bool>() {
|
||||||
Ok(value) => Ok(value),
|
Ok(normalized) => Ok(normalized),
|
||||||
Err(e) => Err(EnvError::invalid("bool", value, &e.to_string()))
|
Err(e) => Err(EnvError::invalid("bool", normalized, &e.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,3 +50,17 @@ pub fn parse_number<T: Number>(value: &str) -> Result<T, EnvError> {
|
|||||||
Err(e) => Err(EnvError::invalid(type_name::<T>(), value, &e.to_string()))
|
Err(e) => Err(EnvError::invalid(type_name::<T>(), value, &e.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses a comma-separated list of values of the specified type.
|
||||||
|
pub fn parse_list<T: FromEnv>(value: &str) -> Result<Vec<T>, EnvError> {
|
||||||
|
let items = value.split(',').map(|item| item.trim());
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for item in items {
|
||||||
|
match T::from_env(item) {
|
||||||
|
Ok(value) => result.push(value),
|
||||||
|
Err(e) => return Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
+142
-25
@@ -1,43 +1,160 @@
|
|||||||
#[cfg(test)]
|
use envkit::{EnvLoader, error::EnvError};
|
||||||
mod test_main {
|
use std::path::PathBuf;
|
||||||
use envkit::*;
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
#[test]
|
fn unique_key(name: &str) -> String {
|
||||||
/// Tests that `get` successfully retrieves an existing environment variable.
|
let nanos = SystemTime::now()
|
||||||
fn test_get() {
|
.duration_since(UNIX_EPOCH)
|
||||||
let env = EnvLoader;
|
.expect("system time should be after the Unix epoch")
|
||||||
|
.as_nanos();
|
||||||
|
|
||||||
unsafe {
|
format!("ENVKIT_TEST_{name}_{nanos}")
|
||||||
std::env::set_var("TEST_VAR", "test_value");
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(env.get::<String>("TEST_VAR"), Ok("test_value".to_string()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Tests that `get_or` returns the default value when the environment variable is missing.
|
fn get_reads_required_typed_values() {
|
||||||
fn test_get_or() {
|
let env = EnvLoader::new();
|
||||||
let env = EnvLoader;
|
let name_key = unique_key("NAME");
|
||||||
|
let port_key = unique_key("PORT");
|
||||||
|
let debug_key = unique_key("DEBUG");
|
||||||
|
let list_key = unique_key("PORTS");
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
std::env::remove_var("MISSING_VAR");
|
std::env::set_var(&name_key, "envkit");
|
||||||
std::env::set_var("TEST_VAR", "test_value");
|
std::env::set_var(&port_key, "3000");
|
||||||
|
std::env::set_var(&debug_key, "yes");
|
||||||
|
std::env::set_var(&list_key, "3000, 3001, 3002");
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(env.get_or::<String>("MISSING_VAR", "default".to_string()), "default".to_string());
|
assert_eq!(env.get::<String>(&name_key), Ok("envkit".to_string()));
|
||||||
assert_eq!(env.get_or::<String>("TEST_VAR", "default".to_string()), "test_value".to_string());
|
assert_eq!(env.get::<u16>(&port_key), Ok(3000));
|
||||||
|
assert_eq!(env.get::<bool>(&debug_key), Ok(true));
|
||||||
|
assert_eq!(env.get::<Vec<u16>>(&list_key), Ok(vec![3000, 3001, 3002]));
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var(name_key);
|
||||||
|
std::env::remove_var(port_key);
|
||||||
|
std::env::remove_var(debug_key);
|
||||||
|
std::env::remove_var(list_key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Tests that `get_opt` returns `Some` when the environment variable exists.
|
fn get_reports_missing_and_invalid_values() {
|
||||||
fn test_get_opt() {
|
let env = EnvLoader::new();
|
||||||
let env = EnvLoader;
|
let missing_key = unique_key("MISSING");
|
||||||
|
let invalid_key = unique_key("INVALID");
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
std::env::set_var("OPTIONAL_VAR", "optional_value");
|
std::env::remove_var(&missing_key);
|
||||||
|
std::env::set_var(&invalid_key, "not-a-port");
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(env.get_opt::<String>("OPTIONAL_VAR"), Some("optional_value".to_string()));
|
assert_eq!(
|
||||||
assert_eq!(env.get_opt::<bool>("DEV"), None);
|
env.get::<String>(&missing_key),
|
||||||
|
Err(EnvError::MissingVar(missing_key.clone()))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
env.get::<u16>(&invalid_key),
|
||||||
|
Err(EnvError::Invalid { key, value, .. })
|
||||||
|
if key == "u16" && value == "not-a-port"
|
||||||
|
));
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var(invalid_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_or_and_get_opt_handle_fallbacks() {
|
||||||
|
let env = EnvLoader::new();
|
||||||
|
let present_key = unique_key("PRESENT");
|
||||||
|
let missing_key = unique_key("DEFAULT");
|
||||||
|
let invalid_key = unique_key("BAD_BOOL");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var(&present_key, "42");
|
||||||
|
std::env::remove_var(&missing_key);
|
||||||
|
std::env::set_var(&invalid_key, "maybe");
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(env.get_or::<u8>(&present_key, 7), 42);
|
||||||
|
assert_eq!(env.get_or::<u8>(&missing_key, 7), 7);
|
||||||
|
assert_eq!(env.get_or::<bool>(&invalid_key, false), false);
|
||||||
|
|
||||||
|
assert_eq!(env.get_opt::<u8>(&present_key), Some(42));
|
||||||
|
assert_eq!(env.get_opt::<u8>(&missing_key), None);
|
||||||
|
assert_eq!(env.get_opt::<bool>(&invalid_key), None);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var(present_key);
|
||||||
|
std::env::remove_var(invalid_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prefixed_loader_reads_namespaced_variables() {
|
||||||
|
let env = EnvLoader::new();
|
||||||
|
let prefix = unique_key("APP_");
|
||||||
|
let host_key = format!("{prefix}HOST");
|
||||||
|
let enabled_key = format!("{prefix}ENABLED");
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var(&host_key, "127.0.0.1");
|
||||||
|
std::env::set_var(&enabled_key, "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
let app = env.with_prefix(prefix);
|
||||||
|
|
||||||
|
assert_eq!(app.get::<String>("HOST"), Ok("127.0.0.1".to_string()));
|
||||||
|
assert_eq!(app.get::<bool>("ENABLED"), Ok(true));
|
||||||
|
assert_eq!(app.get_or::<u16>("PORT", 8080), 8080);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var(host_key);
|
||||||
|
std::env::remove_var(enabled_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_file_sets_environment_variables() {
|
||||||
|
let env = EnvLoader::new();
|
||||||
|
let prefix = unique_key("FILE_");
|
||||||
|
let port_key = format!("{prefix}PORT");
|
||||||
|
let debug_key = format!("{prefix}DEBUG");
|
||||||
|
let names_key = format!("{prefix}NAMES");
|
||||||
|
let path = temp_env_file(&prefix, &port_key, &debug_key, &names_key);
|
||||||
|
|
||||||
|
env.load_file(path.to_str().expect("test path should be UTF-8"))
|
||||||
|
.expect("env file should load");
|
||||||
|
|
||||||
|
assert_eq!(env.get::<u16>(&port_key), Ok(8080));
|
||||||
|
assert_eq!(env.get::<bool>(&debug_key), Ok(true));
|
||||||
|
assert_eq!(
|
||||||
|
env.get::<Vec<String>>(&names_key),
|
||||||
|
Ok(vec!["api".to_string(), "worker".to_string()])
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::remove_file(path).expect("test env file should be removable");
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var(port_key);
|
||||||
|
std::env::remove_var(debug_key);
|
||||||
|
std::env::remove_var(names_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn temp_env_file(prefix: &str, port_key: &str, debug_key: &str, names_key: &str) -> PathBuf {
|
||||||
|
let path = std::env::temp_dir().join(format!("{prefix}.env"));
|
||||||
|
let contents = format!(
|
||||||
|
r#"
|
||||||
|
# comments and blank lines are ignored
|
||||||
|
{port_key} = 8080
|
||||||
|
{debug_key}= yes
|
||||||
|
{names_key}= api, worker
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::write(&path, contents).expect("test env file should be writable");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
mod test_parser {
|
mod test_parser {
|
||||||
use envkit::parser::parse_bool;
|
use envkit::parser::parse_bool;
|
||||||
use envkit::parser::parse_number;
|
use envkit::parser::parse_number;
|
||||||
|
use envkit::parser::parse_list;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Tests for the `parse_bool` function.
|
/// Tests for the `parse_bool` function.
|
||||||
@@ -34,4 +35,18 @@ mod test_parser {
|
|||||||
assert_eq!(parse_number::<f32>("42"), Ok(42_f32));
|
assert_eq!(parse_number::<f32>("42"), Ok(42_f32));
|
||||||
assert_eq!(parse_number::<f64>("42"), Ok(42_f64));
|
assert_eq!(parse_number::<f64>("42"), Ok(42_f64));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Tests for the `parse_list` function.
|
||||||
|
fn test_parse_list() {
|
||||||
|
let numbers = "1,2,3,4";
|
||||||
|
let names = "Alice, Bob, Charlie";
|
||||||
|
let bools = "true, false, yes, no";
|
||||||
|
let floats = "3.14, 2.718, 1.618";
|
||||||
|
|
||||||
|
assert_eq!(parse_list::<u8>(numbers), Ok(Vec::from([1,2,3,4])));
|
||||||
|
assert_eq!(parse_list::<String>(names), Ok(Vec::from(["Alice".to_string(), "Bob".to_string(), "Charlie".to_string()])));
|
||||||
|
assert_eq!(parse_list::<bool>(bools), Ok(Vec::from([true, false, true, false])));
|
||||||
|
assert_eq!(parse_list::<f64>(floats), Ok(Vec::from([3.14, 2.718, 1.618])));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user