Skip to main content

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}