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