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}