rust_web_server/config_binding/mod.rs
1//! Typed configuration binding — read environment variables into strongly-typed structs.
2//!
3//! Use `#[derive(Config)]` (requires `macros` feature) to generate a `load()` method
4//! that reads env vars and parses them into the annotated field types.
5//!
6//! # Quick start
7//!
8//! ```rust,ignore
9//! use rust_web_server::Config;
10//!
11//! #[derive(rust_web_server::Config)]
12//! #[config(prefix = "APP_")]
13//! struct MyConfig {
14//! #[config(env = "PORT", default = "8080")]
15//! port: u16,
16//!
17//! #[config(env = "DATABASE_URL")]
18//! database_url: String, // required — Err if env var is absent
19//!
20//! #[config(env = "FEATURE_FLAG")]
21//! feature_flag: Option<bool>, // None if env var is absent or empty
22//! }
23//!
24//! // At startup:
25//! let cfg = MyConfig::load().expect("failed to load config");
26//! println!("listening on port {}", cfg.port);
27//! ```
28//!
29//! # Field derivation rules
30//!
31//! | Field annotation | Env var absent | Env var present |
32//! |---|---|---|
33//! | `#[config(env = "KEY", default = "v")]` | use `"v"` | parse to field type |
34//! | `#[config(env = "KEY")]` (non-Option) | `Err` | parse to field type |
35//! | `#[config(env = "KEY")]` (`Option<T>`) | `Ok(None)` | parse, wrap in `Some` |
36//! | No `#[config]` — uses `PREFIX + SCREAMING_SNAKE_CASE(field)` | same as non-Option rules |
37//!
38//! # Supported types
39//!
40//! All primitive Rust scalar types implement [`FromEnvStr`] and can be used as field types:
41//! `String`, `bool`, `u8`, `u16`, `u32`, `u64`, `u128`, `usize`, `i8`, `i16`, `i32`, `i64`,
42//! `i128`, `isize`, `f32`, `f64`. Wrap in `Option<T>` for optional fields.
43
44#[cfg(test)]
45mod tests;
46
47/// Parse a value from an environment variable string.
48///
49/// Implement this trait to support custom field types in `#[derive(Config)]` structs.
50pub trait FromEnvStr: Sized {
51 fn from_env_str(s: &str) -> Result<Self, String>;
52}
53
54impl FromEnvStr for String {
55 fn from_env_str(s: &str) -> Result<Self, String> {
56 Ok(s.to_string())
57 }
58}
59
60impl FromEnvStr for bool {
61 fn from_env_str(s: &str) -> Result<Self, String> {
62 match s.trim() {
63 "true" | "1" | "yes" => Ok(true),
64 "false" | "0" | "no" => Ok(false),
65 other => Err(format!("expected true/false/1/0/yes/no, got {:?}", other)),
66 }
67 }
68}
69
70macro_rules! impl_from_env_str_via_parse {
71 ($($t:ty),+) => {
72 $(
73 impl FromEnvStr for $t {
74 fn from_env_str(s: &str) -> Result<Self, String> {
75 s.trim().parse::<$t>().map_err(|e| e.to_string())
76 }
77 }
78 )+
79 };
80}
81
82impl_from_env_str_via_parse!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64);
83
84// ── Runtime helpers called by generated `load()` code ────────────────────────
85
86/// Read a required env var and parse it into `T`.
87///
88/// Returns `Err` if the variable is not set or if parsing fails.
89pub fn load_required<T: FromEnvStr>(key: &str) -> Result<T, String> {
90 let val = std::env::var(key)
91 .map_err(|_| format!("required env var `{}` is not set", key))?;
92 T::from_env_str(&val).map_err(|e| format!("`{}`: {}", key, e))
93}
94
95/// Read an env var and parse it into `T`, falling back to `default` when the variable is absent.
96///
97/// Returns `Err` if parsing fails on either the env var value or the default.
98pub fn load_with_default<T: FromEnvStr>(key: &str, default: &str) -> Result<T, String> {
99 let val = std::env::var(key).unwrap_or_else(|_| default.to_string());
100 T::from_env_str(&val).map_err(|e| format!("`{}`: {}", key, e))
101}
102
103/// Read an optional env var and parse it into `Option<T>`.
104///
105/// Returns `Ok(None)` when the variable is absent or empty.
106/// Returns `Err` if the variable is present but parsing fails.
107pub fn load_optional<T: FromEnvStr>(key: &str) -> Result<Option<T>, String> {
108 match std::env::var(key) {
109 Ok(val) if !val.trim().is_empty() => T::from_env_str(&val)
110 .map(Some)
111 .map_err(|e| format!("`{}`: {}", key, e)),
112 _ => Ok(None),
113 }
114}