Skip to main content

dynamic_config/
bindings.rs

1//! Binding one field to one environment variable, by name.
2//!
3//! The [`env`](crate::LoadSpec::env_prefix) layer covers the case where the
4//! variable names are yours to choose: `APP_DB_POOL__MAX_SIZE` follows from the
5//! prefix, the key and the field. It does not cover the case where they are
6//! not.
7//!
8//! ```text
9//! PORT                 the platform picked it — Heroku, Cloud Run, Fly
10//! DATABASE_URL         a convention older than this program
11//! REDIS_URL            an add-on wrote it into the environment
12//! ```
13//!
14//! None of those can be renamed, and none of them fit a prefix. A binding says
15//! *this field comes from that variable*, and nothing else changes:
16//!
17//! ```rust,no_run
18//! # #[cfg(feature = "toml")] {
19//! # use serde::Deserialize;
20//! # #[dynamic_config::dynamic_config(files = ["config.toml"], key = "server", env = "APP_")]
21//! # #[derive(Deserialize)] struct ServerConfig { port: u16 }
22//! ServerConfig::bind_env("port", "PORT")?;
23//!
24//! ServerConfig::init()?;
25//! # }
26//! # Ok::<(), dynamic_config::Error>(())
27//! ```
28//!
29//! # Where it sits
30//!
31//! ```text
32//! defaults < files < remote < APP_SERVER_* < bindings < flags < overrides
33//! ```
34//!
35//! Just above the prefixed environment layer, because a binding is the more
36//! specific statement: somebody named this variable on purpose, and the
37//! prefixed one is a convention.
38//!
39//! # Read at load time, not at binding time
40//!
41//! The variable is looked up during each `load()`, so a reload picks up a
42//! change to it. Binding a variable that is not set contributes nothing — it is
43//! not an error, because the whole point is that the platform may or may not
44//! have set it.
45
46use std::collections::BTreeMap;
47use std::sync::Mutex;
48
49use figment::value::{Dict, Value};
50use figment::{Metadata, Profile, Provider};
51
52use crate::error::Error;
53
54/// The environment variables bound to fields of one configuration type.
55///
56/// `EnvBindings::new()` is `const`, so this lives in a `static` — which is how
57/// `#[dynamic_config]` emits it.
58#[derive(Debug, Default)]
59pub struct EnvBindings {
60    /// Key path → variable name.
61    entries: Mutex<BTreeMap<String, String>>,
62}
63
64impl EnvBindings {
65    /// No bindings.
66    #[must_use]
67    pub const fn new() -> Self {
68        Self {
69            entries: Mutex::new(BTreeMap::new()),
70        }
71    }
72
73    /// Binds the field at `path` to the environment variable `variable`.
74    ///
75    /// Takes effect on the next `load()`. Binding the same path twice replaces
76    /// the first binding rather than layering it: two variables for one field
77    /// would have no defensible order between them.
78    ///
79    /// # Errors
80    ///
81    /// If `path` is empty or has an empty segment — `"a..b"` names nothing.
82    pub fn bind(&self, path: &str, variable: &str) -> Result<(), Error> {
83        crate::layer::check_path(path)?;
84
85        self.lock().insert(path.to_owned(), variable.to_owned());
86
87        Ok(())
88    }
89
90    /// Drops every binding.
91    pub fn clear(&self) {
92        self.lock().clear();
93    }
94
95    /// Whether anything is bound.
96    pub fn is_empty(&self) -> bool {
97        self.lock().is_empty()
98    }
99
100    /// The variable bound to `path`, if any.
101    #[must_use]
102    pub fn variable(&self, path: &str) -> Option<String> {
103        self.lock().get(path).cloned()
104    }
105
106    /// One provider per binding, so a value can be traced to the variable that
107    /// supplied it rather than to "a binding" in general.
108    ///
109    /// figment attaches metadata per provider, not per key, so naming the
110    /// variable means one provider each. There are as many as the program made
111    /// bindings — a handful — and they are built once per load.
112    pub(crate) fn providers(&self, key: &str) -> Vec<BindingProvider> {
113        self.lock()
114            .iter()
115            .map(|(path, variable)| BindingProvider {
116                path: path.clone(),
117                variable: variable.clone(),
118                key: key.to_owned(),
119            })
120            .collect()
121    }
122
123    fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, String>> {
124        // Recovered rather than propagated, as everywhere else in the crate:
125        // the map has no invariant a panic could break.
126        self.entries
127            .lock()
128            .unwrap_or_else(std::sync::PoisonError::into_inner)
129    }
130}
131
132/// Prefixed onto the variable's name, so the loader can recognise this layer
133/// and report the variable rather than a category.
134pub(crate) const BINDING_PREFIX: &str = "the environment variable ";
135
136/// One binding: one path, one variable.
137pub(crate) struct BindingProvider {
138    path: String,
139    variable: String,
140    key: String,
141}
142
143impl Provider for BindingProvider {
144    fn metadata(&self) -> Metadata {
145        Metadata::named(format!("{BINDING_PREFIX}{}", self.variable))
146    }
147
148    fn data(&self) -> figment::Result<figment::value::Map<Profile, Dict>> {
149        let mut values = Dict::new();
150
151        if let Some(value) = resolve(&self.variable) {
152            crate::layer::insert_path(&mut values, &self.path, value);
153        }
154
155        let mut map = figment::value::Map::new();
156        map.insert(Profile::from(self.key.clone()), values);
157
158        Ok(map)
159    }
160}
161
162/// Reads one variable, or `None` if it does not usefully exist.
163fn resolve(variable: &str) -> Option<Value> {
164    let text = std::env::var_os(variable)?;
165    let text = text.to_str()?;
166
167    // Empty is treated as unset, for the same reason the prefixed layer treats
168    // it that way: an unset value rendered into a deployment template leaves
169    // exactly `PORT=`, and letting that blank out a good value is a bad
170    // afternoon.
171    if text.is_empty() {
172        return None;
173    }
174
175    // Parsed the way the environment layer parses: `8080` is a number, `[1,2]`
176    // a list, and anything else a string.
177    Some(
178        text.parse::<Value>()
179            .unwrap_or_else(|_| Value::from(text.to_owned())),
180    )
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn a_path_with_an_empty_segment_is_refused() {
189        let bindings = EnvBindings::new();
190
191        assert!(bindings.bind("pool..max", "X").is_err());
192        assert!(bindings.bind("", "X").is_err());
193        assert!(bindings.bind("pool.max", "X").is_ok());
194    }
195
196    #[test]
197    fn binding_the_same_path_twice_replaces_rather_than_layers() {
198        let bindings = EnvBindings::new();
199
200        bindings.bind("port", "OLD_PORT").unwrap();
201        bindings.bind("port", "PORT").unwrap();
202
203        assert_eq!(bindings.variable("port").as_deref(), Some("PORT"));
204    }
205
206    #[test]
207    fn clearing_removes_everything() {
208        let bindings = EnvBindings::new();
209
210        bindings.bind("port", "PORT").unwrap();
211        assert!(!bindings.is_empty());
212
213        bindings.clear();
214        assert!(bindings.is_empty());
215    }
216}