rust_config_tree/config_load.rs
1//! High-level runtime loading through Figment and `confique`.
2//!
3//! This module is responsible for discovering the recursive include tree,
4//! merging config files in runtime precedence order, layering schema-declared
5//! environment variables on top, and finally asking `confique` to apply
6//! defaults and validation.
7
8use std::path::Path;
9
10use confique::{Config, Layer};
11use figment::{
12 Figment,
13 providers::{Format, Json, Toml, Yaml},
14};
15
16use crate::{
17 ConfigSource, ConfigTree, ConfigTreeOptions, IncludeOrder, absolutize_lexical,
18 config::{ConfigResult, ConfigSchema},
19 config_env::ConfiqueEnvProvider,
20 config_format::ConfigFormat,
21 config_trace::trace_config_sources,
22};
23
24/// Loads a complete `confique` schema from a root config path.
25///
26/// The loader follows recursive include paths exposed by [`ConfigSchema`],
27/// resolves relative include paths from the declaring file, detects include
28/// cycles, loads the first `.env` file found from the root config directory
29/// upward, builds a [`Figment`] from config files and schema-declared
30/// environment variables, and then asks `confique` to apply defaults and
31/// validation. Existing process environment variables take precedence over
32/// values loaded from `.env`.
33///
34/// # Type Parameters
35///
36/// - `S`: Config schema type that derives [`Config`] and implements
37/// [`ConfigSchema`].
38///
39/// # Arguments
40///
41/// - `path`: Root config file path.
42///
43/// # Returns
44///
45/// Returns the merged config schema after loading the root file, recursive
46/// includes, `.env` values, and environment values.
47///
48/// # Examples
49///
50/// ```
51/// use std::fs;
52/// use confique::Config;
53/// use rust_config_tree::{ConfigSchema, load_config};
54///
55/// #[derive(Debug, Config)]
56/// struct AppConfig {
57/// #[config(default = [])]
58/// include: Vec<std::path::PathBuf>,
59/// #[config(default = "demo")]
60/// mode: String,
61/// }
62///
63/// impl ConfigSchema for AppConfig {
64/// fn include_paths(layer: &<Self as Config>::Layer) -> Vec<std::path::PathBuf> {
65/// layer.include.clone().unwrap_or_default()
66/// }
67/// }
68///
69/// # fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
70/// let path = std::env::temp_dir().join("rust-config-tree-load-config-doctest.yaml");
71/// fs::write(&path, "mode: local\n")?;
72///
73/// let config = load_config::<AppConfig>(&path)?;
74///
75/// assert_eq!(config.mode, "local");
76/// # let _ = fs::remove_file(path);
77/// # Ok(())
78/// # }
79/// # run().unwrap();
80/// ```
81pub fn load_config<S>(path: impl AsRef<Path>) -> ConfigResult<S>
82where
83 S: ConfigSchema,
84{
85 let (config, _) = load_config_with_figment::<S>(path)?;
86 Ok(config)
87}
88
89/// Loads a config schema and returns the Figment graph used for runtime loading.
90///
91/// The returned [`Figment`] can be inspected with [`Figment::find_metadata`] to
92/// determine which provider supplied a runtime value.
93///
94/// # Type Parameters
95///
96/// - `S`: Config schema type that derives [`Config`] and implements
97/// [`ConfigSchema`].
98///
99/// # Arguments
100///
101/// - `path`: Root config file path.
102///
103/// # Returns
104///
105/// Returns the merged config schema and its runtime Figment source graph.
106///
107/// # Examples
108///
109/// ```
110/// use std::fs;
111/// use confique::Config;
112/// use rust_config_tree::{ConfigSchema, load_config_with_figment};
113///
114/// #[derive(Debug, Config)]
115/// struct AppConfig {
116/// #[config(default = [])]
117/// include: Vec<std::path::PathBuf>,
118/// #[config(default = "demo")]
119/// mode: String,
120/// }
121///
122/// impl ConfigSchema for AppConfig {
123/// fn include_paths(layer: &<Self as Config>::Layer) -> Vec<std::path::PathBuf> {
124/// layer.include.clone().unwrap_or_default()
125/// }
126/// }
127///
128/// # fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
129/// let path = std::env::temp_dir().join("rust-config-tree-load-with-figment-doctest.yaml");
130/// fs::write(&path, "mode: local\n")?;
131///
132/// let (config, figment) = load_config_with_figment::<AppConfig>(&path)?;
133///
134/// assert_eq!(config.mode, "local");
135/// # let _ = figment;
136/// # let _ = fs::remove_file(path);
137/// # Ok(())
138/// # }
139/// # run().unwrap();
140/// ```
141pub fn load_config_with_figment<S>(path: impl AsRef<Path>) -> ConfigResult<(S, Figment)>
142where
143 S: ConfigSchema,
144{
145 let figment = build_config_figment::<S>(path)?;
146 let config = load_config_from_figment::<S>(&figment)?;
147
148 Ok((config, figment))
149}
150
151/// Builds the Figment runtime source graph for a config tree.
152///
153/// Config files are merged in include order, then environment variables
154/// declared by [`ConfiqueEnvProvider`] are merged with higher priority.
155///
156/// # Type Parameters
157///
158/// - `S`: Config schema type used to discover includes and environment names.
159///
160/// # Arguments
161///
162/// - `path`: Root config file path.
163///
164/// # Returns
165///
166/// Returns a Figment source graph with file and environment providers.
167///
168/// # Examples
169///
170/// ```
171/// use std::fs;
172/// use confique::Config;
173/// use rust_config_tree::{ConfigSchema, build_config_figment};
174///
175/// #[derive(Debug, Config)]
176/// struct AppConfig {
177/// #[config(default = [])]
178/// include: Vec<std::path::PathBuf>,
179/// #[config(default = "demo")]
180/// mode: String,
181/// }
182///
183/// impl ConfigSchema for AppConfig {
184/// fn include_paths(layer: &<Self as Config>::Layer) -> Vec<std::path::PathBuf> {
185/// layer.include.clone().unwrap_or_default()
186/// }
187/// }
188///
189/// # fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
190/// let path = std::env::temp_dir().join("rust-config-tree-build-figment-doctest.yaml");
191/// fs::write(&path, "mode: local\n")?;
192///
193/// let figment = build_config_figment::<AppConfig>(&path)?;
194/// # let _ = figment;
195/// # let _ = fs::remove_file(path);
196/// # Ok(())
197/// # }
198/// # run().unwrap();
199/// ```
200pub fn build_config_figment<S>(path: impl AsRef<Path>) -> ConfigResult<Figment>
201where
202 S: ConfigSchema,
203{
204 let path = path.as_ref();
205 load_dotenv_for_path(path)?;
206
207 let tree = load_layer_tree::<S>(path)?;
208 let mut figment = Figment::new();
209
210 for node in tree.nodes().iter().rev() {
211 figment = merge_file_provider(figment, node.path());
212 }
213
214 Ok(figment.merge(ConfiqueEnvProvider::new::<S>()))
215}
216
217/// Extracts and validates a config schema from a Figment source graph.
218///
219/// Figment supplies runtime values. `confique` supplies code defaults and final
220/// validation.
221///
222/// # Type Parameters
223///
224/// - `S`: Config schema type to extract and validate.
225///
226/// # Arguments
227///
228/// - `figment`: Runtime source graph.
229///
230/// # Returns
231///
232/// Returns the final config schema.
233///
234/// # Examples
235///
236/// ```
237/// use std::fs;
238/// use confique::Config;
239/// use rust_config_tree::{ConfigSchema, build_config_figment, load_config_from_figment};
240///
241/// #[derive(Debug, Config)]
242/// struct AppConfig {
243/// #[config(default = [])]
244/// include: Vec<std::path::PathBuf>,
245/// #[config(default = "demo")]
246/// mode: String,
247/// }
248///
249/// impl ConfigSchema for AppConfig {
250/// fn include_paths(layer: &<Self as Config>::Layer) -> Vec<std::path::PathBuf> {
251/// layer.include.clone().unwrap_or_default()
252/// }
253/// }
254///
255/// # fn run() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
256/// let path = std::env::temp_dir().join("rust-config-tree-load-from-figment-doctest.yaml");
257/// fs::write(&path, "mode: local\n")?;
258/// let figment = build_config_figment::<AppConfig>(&path)?;
259///
260/// let config = load_config_from_figment::<AppConfig>(&figment)?;
261///
262/// assert_eq!(config.mode, "local");
263/// # let _ = fs::remove_file(path);
264/// # Ok(())
265/// # }
266/// # run().unwrap();
267/// ```
268pub fn load_config_from_figment<S>(figment: &Figment) -> ConfigResult<S>
269where
270 S: ConfigSchema,
271{
272 let runtime_layer: <S as Config>::Layer = figment.extract()?;
273 let config = S::from_layer(runtime_layer.with_fallback(S::Layer::default_values()))?;
274
275 trace_config_sources::<S>(figment);
276
277 Ok(config)
278}
279
280/// Loads one config layer from disk using the format inferred from the path.
281///
282/// # Type Parameters
283///
284/// - `S`: Config schema type whose intermediate `confique` layer should be
285/// loaded.
286///
287/// # Arguments
288///
289/// - `path`: Config file path to load.
290///
291/// # Returns
292///
293/// Returns the loaded `confique` layer for `S`.
294///
295/// # Examples
296///
297/// ```no_run
298/// let _ = ();
299/// ```
300pub(crate) fn load_layer<S>(path: &Path) -> ConfigResult<<S as Config>::Layer>
301where
302 S: ConfigSchema,
303{
304 Ok(figment_for_file(path).extract()?)
305}
306
307/// Loads every config layer reachable from the root include tree.
308///
309/// # Type Parameters
310///
311/// - `S`: Config schema type whose layer type is loaded for each file.
312///
313/// # Arguments
314///
315/// - `path`: Root config path used to start include traversal.
316///
317/// # Returns
318///
319/// Returns the loaded config tree containing one `confique` layer per source.
320///
321/// # Examples
322///
323/// ```no_run
324/// let _ = ();
325/// ```
326fn load_layer_tree<S>(path: &Path) -> ConfigResult<ConfigTree<<S as Config>::Layer>>
327where
328 S: ConfigSchema,
329{
330 // Reverse traversal lets later declared includes override earlier files
331 // after the collected nodes are merged from leaves back toward the root.
332 Ok(ConfigTreeOptions::default()
333 .include_order(IncludeOrder::Reverse)
334 .load(
335 path,
336 |path| -> ConfigResult<ConfigSource<<S as Config>::Layer>> {
337 let layer = load_layer::<S>(path)?;
338 let include_paths = S::include_paths(&layer);
339 Ok(ConfigSource::new(layer, include_paths))
340 },
341 )?)
342}
343
344/// Merges one file provider selected from the path extension.
345///
346/// # Arguments
347///
348/// - `figment`: Existing Figment graph to extend.
349/// - `path`: Config file path whose extension selects the provider format.
350///
351/// # Returns
352///
353/// Returns `figment` with the selected file provider merged in.
354///
355/// # Examples
356///
357/// ```no_run
358/// let _ = ();
359/// ```
360fn merge_file_provider(figment: Figment, path: &Path) -> Figment {
361 match ConfigFormat::from_path(path) {
362 ConfigFormat::Yaml => figment.merge(Yaml::file_exact(path)),
363 ConfigFormat::Toml => figment.merge(Toml::file_exact(path)),
364 ConfigFormat::Json => figment.merge(Json::file_exact(path)),
365 }
366}
367
368/// Builds a Figment graph containing only one config file provider.
369///
370/// # Arguments
371///
372/// - `path`: Config file path to load through Figment.
373///
374/// # Returns
375///
376/// Returns a Figment graph containing exactly that file provider.
377///
378/// # Examples
379///
380/// ```no_run
381/// let _ = ();
382/// ```
383pub(crate) fn figment_for_file(path: &Path) -> Figment {
384 merge_file_provider(Figment::new(), path)
385}
386
387/// Loads the nearest ancestor `.env` file for a config path when it exists.
388///
389/// # Arguments
390///
391/// - `path`: Config file path whose ancestors should be searched.
392///
393/// # Returns
394///
395/// Returns `Ok(())` after loading the first discovered `.env`, or when none
396/// exists.
397///
398/// # Examples
399///
400/// ```no_run
401/// let _ = ();
402/// ```
403fn load_dotenv_for_path(path: &Path) -> ConfigResult<()> {
404 let path = absolutize_lexical(path)?;
405 let mut current_dir = path.parent();
406
407 while let Some(dir) = current_dir {
408 let dotenv_path = dir.join(".env");
409 if dotenv_path.try_exists()? {
410 // `dotenvy` preserves existing process variables, so explicit
411 // environment values keep precedence over values from `.env`.
412 dotenvy::from_path(&dotenv_path)?;
413 break;
414 }
415 current_dir = dir.parent();
416 }
417
418 Ok(())
419}