rskit_config/loader/
mod.rs1mod decode;
4mod dotenv;
5mod env;
6mod source;
7
8use std::path::PathBuf;
9
10use rskit_errors::{AppError, AppResult};
11use rskit_validation::Validate;
12use serde::de::DeserializeOwned;
13
14use crate::AppConfig;
15
16pub use dotenv::{DotenvFileSource, Profile};
17pub use env::EnvironmentSource;
18pub use source::{ConfigMapSource, ConfigSource, TomlFileSource};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21enum LoaderPolicy {
22 App,
23 Toml,
24 Custom,
25}
26
27#[derive(Debug)]
34pub struct ConfigLoader {
35 policy: LoaderPolicy,
36 defaults: Vec<(String, config::Value)>,
37 config_file: Option<PathBuf>,
38 env_file: Option<PathBuf>,
39 env_prefix: String,
40 profile: Option<Profile>,
41 sources: Vec<Box<dyn ConfigSource>>,
42 overrides: Vec<(String, config::Value)>,
43}
44
45impl Default for ConfigLoader {
46 fn default() -> Self {
47 Self::app()
48 }
49}
50
51impl ConfigLoader {
52 pub fn new() -> Self {
54 Self::app()
55 }
56
57 pub fn app() -> Self {
59 Self {
60 policy: LoaderPolicy::App,
61 defaults: Vec::new(),
62 config_file: None,
63 env_file: None,
64 env_prefix: String::new(),
65 profile: None,
66 sources: Vec::new(),
67 overrides: Vec::new(),
68 }
69 }
70
71 pub fn toml(path: impl Into<PathBuf>) -> Self {
75 Self {
76 policy: LoaderPolicy::Toml,
77 defaults: Vec::new(),
78 config_file: Some(path.into()),
79 env_file: None,
80 env_prefix: String::new(),
81 profile: None,
82 sources: Vec::new(),
83 overrides: Vec::new(),
84 }
85 }
86
87 pub fn custom() -> Self {
89 Self {
90 policy: LoaderPolicy::Custom,
91 defaults: Vec::new(),
92 config_file: None,
93 env_file: None,
94 env_prefix: String::new(),
95 profile: None,
96 sources: Vec::new(),
97 overrides: Vec::new(),
98 }
99 }
100
101 #[must_use]
105 pub fn with_default(mut self, key: impl Into<String>, value: impl Into<config::Value>) -> Self {
106 self.defaults.push((key.into(), value.into()));
107 self
108 }
109
110 #[must_use]
112 pub fn with_config_file(mut self, path: impl Into<PathBuf>) -> Self {
113 self.config_file = Some(path.into());
114 self
115 }
116
117 #[must_use]
119 pub fn with_env_file(mut self, path: impl Into<PathBuf>) -> Self {
120 self.env_file = Some(path.into());
121 self
122 }
123
124 #[must_use]
128 pub fn with_env_prefix(mut self, prefix: impl Into<String>) -> Self {
129 self.env_prefix = prefix.into();
130 self
131 }
132
133 #[must_use]
139 pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
140 let profile = profile.into();
141 self.profile = Some(if profile.is_empty() {
142 Profile::FromEnvironment
143 } else {
144 Profile::Name(profile)
145 });
146 self
147 }
148
149 #[must_use]
155 pub fn with_source(mut self, source: impl ConfigSource) -> Self {
156 self.sources.push(Box::new(source));
157 self
158 }
159
160 #[must_use]
164 pub fn with_override(
165 mut self,
166 key: impl Into<String>,
167 value: impl Into<config::Value>,
168 ) -> Self {
169 self.overrides.push((key.into(), value.into()));
170 self
171 }
172
173 pub fn load<T>(&self) -> AppResult<T>
175 where
176 T: DeserializeOwned + Validate,
177 {
178 self.load_with(|_| {})
179 }
180
181 pub fn load_with<T>(&self, apply_defaults: impl FnOnce(&mut T)) -> AppResult<T>
183 where
184 T: DeserializeOwned + Validate,
185 {
186 decode::decode(self.collect()?, apply_defaults)
187 }
188
189 pub fn load_app<T>(&self) -> AppResult<T>
191 where
192 T: AppConfig,
193 {
194 self.load_with(T::apply_defaults)
195 }
196
197 fn collect(&self) -> AppResult<config::Config> {
198 let mut builder = config::Config::builder();
199
200 for (key, value) in &self.defaults {
201 builder = builder
202 .set_default(key, value.clone())
203 .map_err(|e| AppError::invalid_input("config", e.to_string()))?;
204 }
205
206 for source in self.policy_sources()? {
207 builder = builder.add_source(source.collect()?);
208 }
209
210 for source in &self.sources {
211 builder = builder.add_source(source.collect()?);
212 }
213
214 if self.policy == LoaderPolicy::App {
215 builder =
216 builder.add_source(EnvironmentSource::with_prefix(&self.env_prefix).collect()?);
217 }
218
219 for (key, value) in &self.overrides {
220 builder = builder
221 .set_override(key, value.clone())
222 .map_err(|e| AppError::invalid_input("config", e.to_string()))?;
223 }
224
225 builder
226 .build()
227 .map_err(|e| AppError::invalid_input("config", e.to_string()))
228 }
229
230 fn policy_sources(&self) -> AppResult<Vec<Box<dyn ConfigSource>>> {
231 match self.policy {
232 LoaderPolicy::App => self.app_policy_sources(),
233 LoaderPolicy::Toml => {
234 let path = self.config_file.as_ref().ok_or_else(|| {
235 AppError::invalid_input("config", "TOML loader requires a config file")
236 })?;
237 Ok(vec![Box::new(TomlFileSource::required(path.clone()))])
238 }
239 LoaderPolicy::Custom => Ok(Vec::new()),
240 }
241 }
242
243 fn app_policy_sources(&self) -> AppResult<Vec<Box<dyn ConfigSource>>> {
244 let mut sources: Vec<Box<dyn ConfigSource>> = Vec::new();
245
246 if let Some(path) = &self.config_file {
247 sources.push(Box::new(TomlFileSource::optional(path.clone())));
248 } else {
249 sources.push(Box::new(TomlFileSource::optional("config.toml")));
250 sources.push(Box::new(TomlFileSource::optional("config/config.toml")));
251 }
252
253 if let Some(profile) = &self.profile {
254 let profile_name = profile.resolve()?;
255 let path = dotenv::find_profile_env_file(profile_name.as_ref()).ok_or_else(|| {
256 AppError::invalid_input(
257 "config",
258 format!("profile env file not found for profile '{profile_name}'"),
259 )
260 })?;
261 sources.push(Box::new(DotenvFileSource::profile(
262 path,
263 self.env_prefix.clone(),
264 )));
265 }
266
267 if let Some(path) = &self.env_file {
268 sources.push(Box::new(DotenvFileSource::required(
269 path.clone(),
270 self.env_prefix.clone(),
271 )));
272 } else if let Some(path) = dotenv::find_default_env_file() {
273 sources.push(Box::new(DotenvFileSource::auto_discovered(
274 path,
275 self.env_prefix.clone(),
276 )));
277 }
278
279 Ok(sources)
280 }
281}
282
283pub fn load_config<T: AppConfig>() -> AppResult<T> {
285 ConfigLoader::app().load_app()
286}