tako_rs_core/config.rs
1//! Configuration loading from environment variables.
2//!
3//! Provides a `Config<T>` wrapper that can be loaded from environment variables
4//! and injected as router state for access in handlers.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use tako::config::Config;
10//! use serde::Deserialize;
11//!
12//! #[derive(Deserialize, Clone)]
13//! struct AppConfig {
14//! database_url: String,
15//! port: u16,
16//! debug: bool,
17//! }
18//!
19//! // Load from environment variables (DATABASE_URL, PORT, DEBUG)
20//! // let config = Config::<AppConfig>::from_env().expect("missing config");
21//! ```
22
23use serde::de::DeserializeOwned;
24
25/// A typed configuration wrapper loaded from environment variables.
26///
27/// `Config<T>` reads environment variables and deserializes them into a
28/// strongly-typed struct. Variable names are matched by converting struct field names
29/// to `SCREAMING_SNAKE_CASE`.
30#[derive(Debug, Clone)]
31pub struct Config<T: Clone>(pub T);
32
33impl<T: DeserializeOwned + Clone> Config<T> {
34 /// Loads configuration from environment variables.
35 ///
36 /// Field names are matched against environment variable names case-insensitively
37 /// (`database_url` ↔ `DATABASE_URL`). Non-string fields (`u16`, `bool`, …) are
38 /// parsed via the `envy` crate's per-field deserializers, so a typed
39 /// `port: u16` reads `PORT=8080` natively without relying on JSON number
40 /// coercion (which the previous serde_json-roundtrip implementation got wrong).
41 pub fn from_env() -> Result<Self, ConfigError> {
42 let config: T = envy::from_env::<T>().map_err(|e| ConfigError(e.to_string()))?;
43 Ok(Config(config))
44 }
45
46 /// Loads configuration from environment variables that share a common prefix.
47 ///
48 /// Useful when several configs coexist in the process — set
49 /// `MYAPP_DATABASE_URL`, `MYAPP_PORT`, … and call `Config::from_env_prefixed("MYAPP_")`.
50 pub fn from_env_prefixed(prefix: &str) -> Result<Self, ConfigError> {
51 let config: T = envy::prefixed(prefix)
52 .from_env::<T>()
53 .map_err(|e| ConfigError(e.to_string()))?;
54 Ok(Config(config))
55 }
56
57 /// Creates a Config from an existing value.
58 pub fn new(config: T) -> Self {
59 Config(config)
60 }
61
62 /// Returns a reference to the inner config value.
63 pub fn inner(&self) -> &T {
64 &self.0
65 }
66
67 /// Consumes the wrapper and returns the inner value.
68 pub fn into_inner(self) -> T {
69 self.0
70 }
71}
72
73/// Error type for configuration loading.
74#[derive(Debug, Clone)]
75pub struct ConfigError(pub String);
76
77impl std::fmt::Display for ConfigError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(f, "configuration error: {}", self.0)
80 }
81}
82
83impl std::error::Error for ConfigError {}