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