1use std::collections::BTreeMap;
2
3use serde::{Serialize, de::DeserializeOwned};
4
5use crate::{
6 crypto::{Encryptor, decrypt_fields},
7 deserialize::ConfigValueDeserializer,
8 error::LoadError,
9 merge::{FieldStrategies, merge},
10 migrate::MigrationChain,
11 source::Source,
12 value::ConfigValue,
13};
14
15#[cfg(feature = "async")]
16use crate::source::AsyncSource;
17
18pub trait ConfigSchema: DeserializeOwned + Serialize {
22 const APP_NAME: &'static str;
23 const SCHEMA_VERSION: u32;
24
25 #[must_use]
27 fn field_strategies() -> FieldStrategies {
28 &[]
29 }
30
31 #[must_use]
33 fn encrypted_fields() -> &'static [&'static str] {
34 &[]
35 }
36}
37
38pub(crate) struct LoaderEntry {
44 pub(crate) source: std::sync::Arc<dyn Source + Send + Sync>,
45 #[cfg(feature = "hot-reload")]
46 pub(crate) watch_paths: Vec<std::path::PathBuf>,
47}
48
49pub struct ConfigLoader<S: ConfigSchema> {
55 entries: Vec<LoaderEntry>,
56 migrations: Option<std::sync::Arc<MigrationChain>>,
57 encryptor: Option<std::sync::Arc<dyn Encryptor>>,
58 _phantom: std::marker::PhantomData<S>,
59}
60
61impl<S: ConfigSchema> Clone for ConfigLoader<S> {
62 fn clone(&self) -> Self {
63 Self {
64 entries: self
65 .entries
66 .iter()
67 .map(|e| LoaderEntry {
68 source: std::sync::Arc::clone(&e.source),
69 #[cfg(feature = "hot-reload")]
70 watch_paths: e.watch_paths.clone(),
71 })
72 .collect(),
73 migrations: self.migrations.clone(),
74 encryptor: self.encryptor.clone(),
75 _phantom: std::marker::PhantomData,
76 }
77 }
78}
79
80impl<S: ConfigSchema> Default for ConfigLoader<S> {
81 fn default() -> Self {
82 Self {
83 entries: Vec::new(),
84 migrations: None,
85 encryptor: None,
86 _phantom: std::marker::PhantomData,
87 }
88 }
89}
90
91impl<S: ConfigSchema> ConfigLoader<S> {
92 #[must_use]
100 pub fn with_source(mut self, source: impl Source + Send + Sync + 'static) -> Self {
101 let source = source.with_app_name(S::APP_NAME);
102 self.entries.push(LoaderEntry {
103 source: std::sync::Arc::new(source),
104 #[cfg(feature = "hot-reload")]
105 watch_paths: Vec::new(),
106 });
107 self
108 }
109
110 #[cfg(feature = "hot-reload")]
117 #[must_use]
118 pub fn with_watched_source(
119 mut self,
120 source: impl crate::source::WatchableSource + 'static,
121 ) -> Self {
122 let source = source.with_app_name(S::APP_NAME);
123 let watch_paths = source.watch_paths();
124 self.entries.push(LoaderEntry {
125 source: std::sync::Arc::new(source),
126 watch_paths,
127 });
128 self
129 }
130
131 #[must_use]
132 pub fn with_migrations(mut self, migrations: MigrationChain) -> Self {
133 self.migrations = Some(std::sync::Arc::new(migrations));
134 self
135 }
136
137 #[must_use]
138 pub fn with_encryptor(mut self, encryptor: impl Encryptor + 'static) -> Self {
139 self.encryptor = Some(std::sync::Arc::new(encryptor));
140 self
141 }
142
143 pub fn load(&self) -> Result<S, LoadError> {
148 let strategies = S::field_strategies();
149
150 let mut merged = ConfigValue::Table(BTreeMap::new());
151 for entry in &self.entries {
152 let collected = entry.source.collect()?;
153 merged = merge(merged, collected, strategies);
154 }
155
156 let current_version = match &merged {
157 ConfigValue::Table(t) => match t.get("__schema_version") {
158 Some(ConfigValue::Integer(v)) => u32::try_from(*v).unwrap_or(0),
159 _ => 0,
160 },
161 _ => 0,
162 };
163
164 if current_version < S::SCHEMA_VERSION
165 && let Some(chain) = &self.migrations
166 {
167 merged = chain.run(merged, current_version, S::SCHEMA_VERSION)?;
168 }
169
170 if let Some(enc) = &self.encryptor {
171 decrypt_fields(&mut merged, S::encrypted_fields(), enc.as_ref())?;
172 }
173
174 Ok(S::deserialize(ConfigValueDeserializer(merged))?)
175 }
176}
177
178#[cfg(feature = "hot-reload")]
179impl<S: ConfigSchema + Clone + Send + Sync + 'static> ConfigLoader<S> {
180 pub fn watch(
192 self,
193 watch_path: &std::path::Path,
194 options: crate::handle::WatchOptions,
195 ) -> Result<
196 (crate::handle::ConfigHandle<S>, crate::handle::WatcherHandle),
197 crate::handle::HotReloadError,
198 > {
199 use std::sync::Arc;
200
201 let initial = self.load()?;
202 let loader = Arc::new(self);
203 let reload: Arc<dyn Fn() -> Result<S, LoadError> + Send + Sync> =
204 Arc::new(move || loader.load());
205
206 crate::handle::ConfigHandle::spawn_watcher(
207 initial,
208 watch_path,
209 &reload,
210 None,
211 options.debounce,
212 )
213 }
214
215 pub fn watch_auto(
236 self,
237 options: crate::handle::WatchOptions,
238 ) -> Result<
239 (crate::handle::ConfigHandle<S>, crate::handle::WatcherHandle),
240 crate::handle::HotReloadError,
241 > {
242 use std::sync::Arc;
243
244 use crate::handle::SelectiveEntry;
245
246 let strategies = S::field_strategies();
247 let migrations = self.migrations.clone();
248 let encryptor = self.encryptor.clone();
249
250 let mut selective_entries = Vec::with_capacity(self.entries.len());
251 let mut initial_merged = ConfigValue::Table(BTreeMap::new());
252
253 for entry in &self.entries {
254 let cache = entry.source.collect().map_err(LoadError::from)?;
255 initial_merged = merge(initial_merged, cache.clone(), strategies);
256 selective_entries.push(SelectiveEntry {
257 source: Arc::clone(&entry.source),
258 cache: std::sync::Mutex::new(cache),
259 watch_paths: entry.watch_paths.clone(),
260 });
261 }
262
263 let transform: Arc<dyn Fn(ConfigValue) -> Result<S, LoadError> + Send + Sync> =
264 Arc::new(move |mut merged: ConfigValue| {
265 let current_version = match &merged {
266 ConfigValue::Table(t) => match t.get("__schema_version") {
267 Some(ConfigValue::Integer(v)) => u32::try_from(*v).unwrap_or(0),
268 _ => 0,
269 },
270 _ => 0,
271 };
272 if current_version < S::SCHEMA_VERSION
273 && let Some(chain) = &migrations
274 {
275 merged = chain.run(merged, current_version, S::SCHEMA_VERSION)?;
276 }
277 if let Some(enc) = &encryptor {
278 decrypt_fields(&mut merged, S::encrypted_fields(), enc.as_ref())?;
279 }
280 Ok(S::deserialize(ConfigValueDeserializer(merged))?)
281 });
282
283 let initial = transform(initial_merged.clone())?;
284
285 crate::handle::ConfigHandle::spawn_selective_watcher(
286 initial,
287 initial_merged,
288 selective_entries,
289 strategies,
290 transform,
291 options.debounce,
292 )
293 }
294}
295
296#[cfg(feature = "async")]
302pub struct AsyncConfigLoader<S: ConfigSchema> {
303 sources: Vec<std::sync::Arc<dyn AsyncSource + Send + Sync>>,
304 migrations: Option<std::sync::Arc<MigrationChain>>,
305 encryptor: Option<std::sync::Arc<dyn Encryptor>>,
306 _phantom: std::marker::PhantomData<S>,
307}
308
309#[cfg(feature = "async")]
310impl<S: ConfigSchema> Default for AsyncConfigLoader<S> {
311 fn default() -> Self {
312 Self {
313 sources: Vec::new(),
314 migrations: None,
315 encryptor: None,
316 _phantom: std::marker::PhantomData,
317 }
318 }
319}
320
321#[cfg(feature = "async")]
322impl<S: ConfigSchema> AsyncConfigLoader<S> {
323 #[must_use]
324 pub fn with_source(mut self, source: impl AsyncSource + 'static) -> Self {
325 self.sources.push(std::sync::Arc::new(source));
326 self
327 }
328
329 #[must_use]
330 pub fn with_migrations(mut self, migrations: MigrationChain) -> Self {
331 self.migrations = Some(std::sync::Arc::new(migrations));
332 self
333 }
334
335 #[must_use]
336 pub fn with_encryptor(mut self, encryptor: impl Encryptor + 'static) -> Self {
337 self.encryptor = Some(std::sync::Arc::new(encryptor));
338 self
339 }
340
341 pub async fn load(&self) -> Result<S, LoadError> {
346 let strategies = S::field_strategies();
347
348 let mut merged = ConfigValue::Table(std::collections::BTreeMap::new());
349 for source in &self.sources {
350 let collected = source.collect().await?;
351 merged = merge(merged, collected, strategies);
352 }
353
354 let current_version = match &merged {
355 ConfigValue::Table(t) => match t.get("__schema_version") {
356 Some(ConfigValue::Integer(v)) => u32::try_from(*v).unwrap_or(0),
357 _ => 0,
358 },
359 _ => 0,
360 };
361
362 if current_version < S::SCHEMA_VERSION
363 && let Some(chain) = &self.migrations
364 {
365 merged = chain.run(merged, current_version, S::SCHEMA_VERSION)?;
366 }
367
368 if let Some(enc) = &self.encryptor {
369 decrypt_fields(&mut merged, S::encrypted_fields(), enc.as_ref())?;
370 }
371
372 Ok(S::deserialize(ConfigValueDeserializer(merged))?)
373 }
374}