smart_config/
fallback.rs

1//! Fallback [`Value`] sources.
2//!
3//! # Motivation and use cases
4//!
5//! Some configuration params may be sourced from places that do not fit well into the hierarchical config schema.
6//! For example, a config param with logging directives may want to read from a `RUST_LOG` env var, regardless of where
7//! the param is placed in the hierarchy. It is possible to manually move raw config values around, it may get unmaintainable
8//! for large configs.
9//!
10//! *Fallbacks* provide a more sound approach: declare the fallback config sources as a part of the [`DescribeConfig`](macro@crate::DescribeConfig)
11//! derive macro. In this way, fallbacks are documented (being a part of the config metadata)
12//! and do not require splitting logic between config declaration and preparing config sources.
13//!
14//! Fallbacks should be used sparingly, since they make it more difficult to reason about configs due to their non-local nature.
15//!
16//! # Features and limitations
17//!
18//! - By design, fallbacks are location-independent. E.g., an [`Env`] fallback will always read from the same env var,
19//!   regardless of where the param containing it is placed (including the case when it has multiple copies!).
20//! - Fallbacks always have lower priority than all other config sources.
21
22use std::{collections::HashMap, env, fmt, sync::Arc};
23
24use crate::{
25    ConfigSchema, ConfigSource,
26    source::Hierarchical,
27    testing::MOCK_ENV_VARS,
28    value::{Map, Pointer, Value, ValueOrigin, WithOrigin},
29};
30
31/// Fallback source of a configuration param.
32pub trait FallbackSource: 'static + Send + Sync + fmt::Debug + fmt::Display {
33    /// Potentially provides a value for the param.
34    ///
35    /// Implementations should return `None` (vs `Some(Value::Null)` etc.) if the source doesn't have a value.
36    fn provide_value(&self) -> Option<WithOrigin>;
37}
38
39/// Gets a string value from the specified env variable.
40///
41/// # Examples
42///
43/// ```
44/// use smart_config::{fallback, testing, DescribeConfig, DeserializeConfig};
45///
46/// #[derive(DescribeConfig, DeserializeConfig)]
47/// struct TestConfig {
48///     /// Log directives. Always read from `RUST_LOG` env var in addition to
49///     /// the conventional sources.
50///     #[config(default_t = "info".into(), fallback = &fallback::Env("RUST_LOG"))]
51///     log_directives: String,
52/// }
53///
54/// let mut tester = testing::Tester::default();
55/// let config: TestConfig = tester.test(smart_config::config!())?;
56/// // Without env var set or other sources, the param will assume the default value.
57/// assert_eq!(config.log_directives, "info");
58///
59/// tester.set_env("RUST_LOG", "warn");
60/// let config: TestConfig = tester.test(smart_config::config!())?;
61/// assert_eq!(config.log_directives, "warn");
62///
63/// // Mock env vars are still set here, but fallbacks have lower priority
64/// // than other sources.
65/// let input = smart_config::config!("log_directives": "info,my_crate=debug");
66/// let config = tester.test(input)?;
67/// assert_eq!(config.log_directives, "info,my_crate=debug");
68/// # anyhow::Ok(())
69/// ```
70#[derive(Debug, Clone, Copy)]
71pub struct Env(pub &'static str);
72
73impl fmt::Display for Env {
74    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(formatter, "env var {:?}", self.0)
76    }
77}
78
79impl Env {
80    /// Gets the raw string value of the env var, taking [mock vars] into account.
81    ///
82    /// [mock vars]: crate::testing::Tester::set_env()
83    pub fn get_raw(&self) -> Option<String> {
84        MOCK_ENV_VARS
85            .with(|cell| cell.borrow().get(self.0).cloned())
86            .or_else(|| env::var(self.0).ok())
87    }
88}
89
90impl FallbackSource for Env {
91    fn provide_value(&self) -> Option<WithOrigin> {
92        if let Some(value) = self.get_raw() {
93            let origin = ValueOrigin::Path {
94                source: Arc::new(ValueOrigin::EnvVars),
95                path: self.0.into(),
96            };
97            Some(WithOrigin::new(value.into(), Arc::new(origin)))
98        } else {
99            None
100        }
101    }
102}
103
104/// Custom [fallback value provider](FallbackSource).
105///
106/// # Use cases
107///
108/// This provider is useful when configuration parameter deserialization logic is hard to express
109/// using conventional methods, such as:
110///
111/// - Composite configuration parameters that rely on several environment variables.
112///   In this case, you can use the getter closure to access variables via [`Env`] and combine
113///   them into a single value.
114/// - Configuration values that need additional validation to make the configuration object
115///   correct by construction. For example, if your config has an optional field, which should
116///   be `None` _either_ if it's absent or set to the `"unset"` value, you can first get it via `Env`,
117///   and then only provide value if it's not equal to `"unset"`. You can think of it as a `filter` or
118///   `map` function in this case.
119///
120/// # Examples
121///
122/// ```
123/// # use std::sync::Arc;
124/// use smart_config::{
125///     fallback, testing, value::{ValueOrigin, WithOrigin},
126///     DescribeConfig, DeserializeConfig,
127/// };
128///
129/// // Value source combining two env variables. It usually makes sense to split off
130/// // the definition like this so that it's more readable.
131/// const COMBINED_VARS: &'static dyn fallback::FallbackSource =
132///     &fallback::Manual::new("$TEST_ENV - $TEST_NETWORK", || {
133///         let env = fallback::Env("TEST_ENV").get_raw()?;
134///         let network = fallback::Env("TEST_NETWORK").get_raw()?;
135///         let origin = Arc::new(ValueOrigin::EnvVars);
136///         Some(WithOrigin::new(format!("{env} - {network}").into(), origin))
137///     });
138///
139/// #[derive(DescribeConfig, DeserializeConfig)]
140/// struct TestConfig {
141///     #[config(default_t = "app".into(), fallback = COMBINED_VARS)]
142///     app: String,
143/// }
144///
145/// let config: TestConfig = testing::Tester::default()
146///     .set_env("TEST_ENV", "stage")
147///     .set_env("TEST_NETWORK", "goerli")
148///     .test(smart_config::config!())?;
149/// assert_eq!(config.app, "stage - goerli");
150/// # anyhow::Ok(())
151/// ```
152#[derive(Debug)]
153pub struct Manual {
154    description: &'static str,
155    getter: fn() -> Option<WithOrigin>,
156}
157
158impl Manual {
159    /// Creates a provider with the specified human-readable description and a getter function.
160    pub const fn new(description: &'static str, getter: fn() -> Option<WithOrigin>) -> Self {
161        Self {
162            description,
163            getter,
164        }
165    }
166}
167
168impl fmt::Display for Manual {
169    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170        formatter.write_str(self.description)
171    }
172}
173
174impl FallbackSource for Manual {
175    fn provide_value(&self) -> Option<WithOrigin> {
176        (self.getter)()
177    }
178}
179
180#[derive(Debug)]
181pub(crate) struct Fallbacks {
182    inner: HashMap<(String, &'static str), WithOrigin>,
183    origin: Arc<ValueOrigin>,
184}
185
186impl Fallbacks {
187    #[tracing::instrument(level = "debug", name = "Fallbacks::new", skip_all)]
188    pub(crate) fn new(schema: &ConfigSchema) -> Option<Self> {
189        let mut inner = HashMap::new();
190        for (prefix, config) in schema.iter_ll() {
191            for param in config.metadata.params {
192                let Some(fallback) = param.fallback else {
193                    continue;
194                };
195                if let Some(mut val) = fallback.provide_value() {
196                    tracing::trace!(
197                        prefix = prefix.0,
198                        config = ?config.metadata.ty,
199                        param = param.rust_field_name,
200                        provider = ?fallback,
201                        "got fallback for param"
202                    );
203
204                    let origin = ValueOrigin::Synthetic {
205                        source: val.origin.clone(),
206                        transform: format!(
207                            "fallback for `{}.{}`",
208                            config.metadata.ty.name_in_code(),
209                            param.rust_field_name,
210                        ),
211                    };
212                    val.origin = Arc::new(origin);
213                    inner.insert((prefix.0.to_owned(), param.name), val);
214                }
215            }
216        }
217
218        if inner.is_empty() {
219            None
220        } else {
221            tracing::debug!(count = inner.len(), "got fallbacks for config params");
222            Some(Self {
223                inner,
224                origin: Arc::new(ValueOrigin::Fallbacks),
225            })
226        }
227    }
228}
229
230impl ConfigSource for Fallbacks {
231    type Kind = Hierarchical;
232
233    fn into_contents(self) -> WithOrigin<Map> {
234        let origin = self.origin;
235        let mut map = WithOrigin::new(Value::Object(Map::new()), origin.clone());
236        for ((prefix, name), value) in self.inner {
237            map.ensure_object(Pointer(&prefix), |_| origin.clone())
238                .insert(name.to_owned(), value);
239        }
240
241        map.map(|value| match value {
242            Value::Object(map) => map,
243            _ => unreachable!(),
244        })
245    }
246}