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