rust_config_tree/config_env.rs
1//! Figment environment provider backed by `confique` metadata.
2//!
3//! Figment's raw environment provider can split keys on delimiters. This module
4//! instead reads exact `#[config(env = "...")]` names from the schema metadata
5//! and maps only those variables to their real field paths.
6
7use std::{collections::HashMap, sync::Arc};
8
9use confique::{
10 Config,
11 meta::{FieldKind, Meta},
12};
13use figment::{
14 Metadata, Profile, Provider,
15 providers::Env,
16 value::{Dict, Map, Uncased},
17};
18
19/// Figment provider that maps environment variables declared in `confique`
20/// schema metadata onto their exact field paths.
21///
22/// This provider reads `#[config(env = "...")]` from [`Config::META`] and
23/// avoids Figment's delimiter-based environment key splitting. Environment
24/// variables such as `APP_DATABASE_POOL_SIZE` can therefore map to a Rust field
25/// named `database.pool_size` without treating the single underscores as nested
26/// separators.
27#[derive(Clone)]
28pub struct ConfiqueEnvProvider {
29 env: Env,
30 path_to_env: Arc<HashMap<String, String>>,
31}
32
33/// Constructors for environment providers derived from `confique` metadata.
34impl ConfiqueEnvProvider {
35 /// Creates an environment provider for a `confique` schema.
36 ///
37 /// # Type Parameters
38 ///
39 /// - `S`: Config schema whose metadata declares environment variable names.
40 ///
41 /// # Arguments
42 ///
43 /// This function has no arguments.
44 ///
45 /// # Returns
46 ///
47 /// Returns a provider that emits only environment variables declared by `S`.
48 ///
49 /// # Examples
50 ///
51 /// ```
52 /// use confique::Config;
53 /// use rust_config_tree::ConfiqueEnvProvider;
54 ///
55 /// #[derive(Config)]
56 /// struct AppConfig {
57 /// #[config(env = "APP_MODE", default = "demo")]
58 /// mode: String,
59 /// }
60 ///
61 /// let provider = ConfiqueEnvProvider::new::<AppConfig>();
62 /// # let _ = provider;
63 /// ```
64 pub fn new<S>() -> Self
65 where
66 S: Config,
67 {
68 let mut env_to_path = HashMap::<String, String>::new();
69 let mut path_to_env = HashMap::<String, String>::new();
70
71 collect_env_mapping(&S::META, "", &mut env_to_path, &mut path_to_env);
72
73 let env_to_path = Arc::new(env_to_path);
74 let path_to_env = Arc::new(path_to_env);
75 let map_for_filter = Arc::clone(&env_to_path);
76
77 let env = Env::raw().filter_map(move |env_key| {
78 let lookup_key = env_key.as_str().to_ascii_uppercase();
79
80 map_for_filter
81 .get(&lookup_key)
82 .cloned()
83 .map(Uncased::from_owned)
84 });
85
86 Self { env, path_to_env }
87 }
88}
89
90/// Supplies Figment data and source labels for schema-declared environment variables.
91impl Provider for ConfiqueEnvProvider {
92 /// Builds metadata used by Figment source tracing.
93 ///
94 /// # Arguments
95 ///
96 /// - `self`: Environment provider whose path-to-variable mapping should be
97 /// exposed in metadata.
98 ///
99 /// # Returns
100 ///
101 /// Returns Figment metadata that renders schema paths as native env names.
102 ///
103 /// # Examples
104 ///
105 /// ```no_run
106 /// let _ = ();
107 /// ```
108 fn metadata(&self) -> Metadata {
109 let path_to_env = Arc::clone(&self.path_to_env);
110
111 Metadata::named("environment variable").interpolater(move |_profile, keys| {
112 let path = keys.join(".");
113
114 path_to_env.get(&path).cloned().unwrap_or(path)
115 })
116 }
117
118 /// Reads configured environment variables into Figment data.
119 ///
120 /// # Arguments
121 ///
122 /// - `self`: Environment provider wrapping the filtered Figment env source.
123 ///
124 /// # Returns
125 ///
126 /// Returns Figment data grouped by profile, or a Figment error.
127 ///
128 /// # Examples
129 ///
130 /// ```no_run
131 /// let _ = ();
132 /// ```
133 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
134 self.env.data()
135 }
136}
137
138/// Recursively maps schema field paths to their declared environment variables.
139///
140/// # Arguments
141///
142/// - `meta`: `confique` metadata node to inspect.
143/// - `prefix`: Dot-separated field path prefix for `meta`.
144/// - `env_to_path`: Output map from uppercase environment names to field paths.
145/// - `path_to_env`: Output map from field paths to declared environment names.
146///
147/// # Returns
148///
149/// Returns no value; both output maps are updated in place.
150///
151/// # Examples
152///
153/// ```no_run
154/// let _ = ();
155/// ```
156fn collect_env_mapping(
157 meta: &'static Meta,
158 prefix: &str,
159 env_to_path: &mut HashMap<String, String>,
160 path_to_env: &mut HashMap<String, String>,
161) {
162 for field in meta.fields {
163 let path = if prefix.is_empty() {
164 field.name.to_owned()
165 } else {
166 format!("{prefix}.{}", field.name)
167 };
168
169 match field.kind {
170 FieldKind::Leaf { env: Some(env), .. } => {
171 // Keep both directions: Figment needs env -> path for loading,
172 // while metadata interpolation needs path -> env for tracing.
173 env_to_path.insert(env.to_ascii_uppercase(), path.clone());
174 path_to_env.insert(path, env.to_owned());
175 }
176 FieldKind::Leaf { env: None, .. } => {}
177 FieldKind::Nested { meta } => {
178 collect_env_mapping(meta, &path, env_to_path, path_to_env);
179 }
180 }
181 }
182}