config_load/
lib.rs

1/*!
2The conditional configuration loader for Rust applications that use the `config` crate.
3
4```rust
5use std::path::Path;
6
7use config_load::config::builder::DefaultState;
8use config_load::config::{ConfigBuilder, Environment};
9use config_load::{ConfigLoader, FileLocation, Load};
10use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Default, Deserialize, Serialize)]
13#[serde(default)]
14struct AppConfig {
15    enabled: bool,
16    title: String,
17    attempts: u32,
18}
19
20impl Load for AppConfig {
21    fn load(config_builder: ConfigBuilder<DefaultState>) -> config_load::Result<Self> {
22        let config = config_builder
23            .add_source(Environment::with_prefix("APP").separator("_"))
24            .build()?;
25        config.try_deserialize()
26    }
27}
28
29let config_file = None; // Parsed from command line arguments, for example
30
31let config: AppConfig = ConfigLoader::default()
32    .add(
33        FileLocation::first_some_path()
34            .from_env("APP_ROOT_CONFIG")
35            .from_home(Path::new(".example_app").join("AppConfig.toml")),
36    )
37    .exclude_not_exists() // Exclude file pathes that don't exist, especially when APP_ROOT_CONFIG is set to ""
38    .add(
39        FileLocation::first_some_path()
40            .from_file(config_file)
41            .from_cwd_and_parents_exists("AppConfig.toml"),
42    )
43    .load()
44    .expect("Failed to load config");
45```
46 */
47use std::marker::PhantomData;
48use std::path::PathBuf;
49
50pub use config;
51use config::builder::{AsyncState, BuilderState, DefaultState};
52use config::{Config, ConfigBuilder, ConfigError};
53use either::Either;
54
55pub use crate::load::Load;
56pub use crate::location::Location;
57pub use crate::location::file::FileLocation;
58
59pub mod load;
60pub mod location;
61
62pub type Result<T> = std::result::Result<T, ConfigError>;
63
64#[derive(Debug, Clone)]
65pub struct ConfigLoader<S: BuilderState = DefaultState> {
66    config_paths: Vec<PathBuf>,
67    _state: PhantomData<S>,
68}
69
70impl<S: BuilderState> ConfigLoader<S> {
71    pub fn new() -> Self {
72        Self {
73            config_paths: Vec::new(),
74            _state: PhantomData,
75        }
76    }
77
78    pub fn add(mut self, location: impl Location) -> Self {
79        match location.try_into_path() {
80            Either::Left(path) => self.config_paths.push(path),
81            Either::Right(_) => (),
82        }
83        self
84    }
85
86    pub fn exclude_not_exists(mut self) -> Self {
87        self.config_paths.retain(|path| path.is_file());
88        self
89    }
90}
91
92impl Default for ConfigLoader<DefaultState> {
93    fn default() -> Self {
94        Self::new_default()
95    }
96}
97
98impl ConfigLoader<DefaultState> {
99    pub fn new_default() -> Self {
100        Self::new()
101    }
102
103    pub fn builder(self) -> ConfigBuilder<DefaultState> {
104        let mut config_builder = Config::builder();
105        for path in self.config_paths {
106            config_builder = config_builder.add_source(config::File::from(path))
107        }
108        config_builder
109    }
110
111    pub fn load<T: Load<DefaultState>>(self) -> Result<T> {
112        let config_builder = self.builder();
113        T::load(config_builder)
114    }
115}
116
117impl ConfigLoader<AsyncState> {
118    pub fn new_async() -> Self {
119        Self::new()
120    }
121
122    pub fn builder(self) -> ConfigBuilder<AsyncState> {
123        let mut config_builder = ConfigBuilder::<AsyncState>::default();
124        for path in self.config_paths {
125            config_builder = config_builder.add_source(config::File::from(path))
126        }
127        config_builder
128    }
129
130    pub fn load<T: Load<AsyncState>>(self) -> Result<T> {
131        let config_builder = self.builder();
132        T::load(config_builder)
133    }
134}