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