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}