1use std::path::PathBuf;
21
22use serde::{Deserialize, Serialize};
23
24use crate::constants::{
25 DEFAULT_CONFIG_BASE_NAME, DEFAULT_ENV_PREFIX, DEFAULT_EXTENSIONS, MAX_SEARCH_DEPTH,
26};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
30#[serde(rename_all = "snake_case")]
31pub enum MergeStrategy {
32 Replace,
34
35 #[default]
37 Deep,
38
39 Shallow,
41
42 Strict,
44}
45
46impl MergeStrategy {
47 #[must_use]
49 pub const fn as_str(&self) -> &'static str {
50 match self {
51 Self::Replace => "replace",
52 Self::Deep => "deep",
53 Self::Shallow => "shallow",
54 Self::Strict => "strict",
55 }
56 }
57}
58
59impl std::fmt::Display for MergeStrategy {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 write!(f, "{}", self.as_str())
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct SourceConfig {
70 pub name: String,
72
73 pub optional: bool,
75
76 pub priority: i32,
78
79 pub cache: bool,
81
82 pub format: Option<String>,
84
85 pub path: Option<PathBuf>,
87
88 pub url: Option<String>,
90
91 pub env_prefix: Option<String>,
93
94 pub extra: std::collections::BTreeMap<String, String>,
96}
97
98impl SourceConfig {
99 #[must_use]
101 pub fn new(name: impl Into<String>) -> Self {
102 Self {
103 name: name.into(),
104 optional: false,
105 priority: 0,
106 cache: true,
107 format: None,
108 path: None,
109 url: None,
110 env_prefix: None,
111 extra: std::collections::BTreeMap::new(),
112 }
113 }
114
115 #[must_use]
117 pub fn builder() -> SourceConfigBuilder {
118 SourceConfigBuilder::new()
119 }
120
121 #[must_use]
123 pub fn file(path: impl Into<PathBuf>) -> Self {
124 Self::builder().name("file").path(path).build()
125 }
126
127 #[must_use]
129 pub fn env(prefix: impl Into<String>) -> Self {
130 Self::builder().name("env").env_prefix(prefix).build()
131 }
132
133 #[must_use]
135 pub fn remote(url: impl Into<String>) -> Self {
136 Self::builder().name("remote").url(url).build()
137 }
138
139 #[must_use]
141 pub const fn is_optional(&self) -> bool {
142 self.optional
143 }
144
145 #[must_use]
147 pub fn display_id(&self) -> String {
148 if let Some(ref path) = self.path {
149 format!("{}:{}", self.name, path.display())
150 } else if let Some(ref url) = self.url {
151 format!("{}:{}", self.name, url)
152 } else if let Some(ref prefix) = self.env_prefix {
153 format!("{}:{}", self.name, prefix)
154 } else {
155 self.name.clone()
156 }
157 }
158}
159
160impl Default for SourceConfig {
161 fn default() -> Self {
162 Self::new(DEFAULT_CONFIG_BASE_NAME)
163 }
164}
165
166#[derive(Debug, Clone, Default)]
168pub struct SourceConfigBuilder {
169 name: Option<String>,
170 optional: bool,
171 priority: i32,
172 cache: bool,
173 format: Option<String>,
174 path: Option<PathBuf>,
175 url: Option<String>,
176 env_prefix: Option<String>,
177 extra: std::collections::BTreeMap<String, String>,
178}
179
180impl SourceConfigBuilder {
181 #[must_use]
183 pub fn new() -> Self {
184 Self {
185 cache: true,
186 ..Self::default()
187 }
188 }
189
190 #[must_use]
192 pub fn name(mut self, name: impl Into<String>) -> Self {
193 self.name = Some(name.into());
194 self
195 }
196
197 #[must_use]
199 pub fn optional(mut self, optional: bool) -> Self {
200 self.optional = optional;
201 self
202 }
203
204 #[must_use]
206 pub fn priority(mut self, priority: i32) -> Self {
207 self.priority = priority;
208 self
209 }
210
211 #[must_use]
213 pub fn cache(mut self, cache: bool) -> Self {
214 self.cache = cache;
215 self
216 }
217
218 #[must_use]
220 pub fn format(mut self, format: impl Into<String>) -> Self {
221 self.format = Some(format.into());
222 self
223 }
224
225 #[must_use]
227 pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
228 self.path = Some(path.into());
229 self
230 }
231
232 #[must_use]
234 pub fn url(mut self, url: impl Into<String>) -> Self {
235 self.url = Some(url.into());
236 self
237 }
238
239 #[must_use]
241 pub fn env_prefix(mut self, prefix: impl Into<String>) -> Self {
242 self.env_prefix = Some(prefix.into());
243 self
244 }
245
246 #[must_use]
248 pub fn extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
249 self.extra.insert(key.into(), value.into());
250 self
251 }
252
253 #[must_use]
255 pub fn build(self) -> SourceConfig {
256 SourceConfig {
257 name: self
258 .name
259 .unwrap_or_else(|| DEFAULT_CONFIG_BASE_NAME.to_string()),
260 optional: self.optional,
261 priority: self.priority,
262 cache: self.cache,
263 format: self.format,
264 path: self.path,
265 url: self.url,
266 env_prefix: self.env_prefix,
267 extra: self.extra,
268 }
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276pub struct LoadOptions {
277 pub merge_strategy: MergeStrategy,
279
280 pub fail_fast: bool,
282
283 pub ignore_optional_missing: bool,
285
286 pub validate: bool,
288
289 pub max_depth: usize,
291
292 pub extensions: Vec<String>,
294
295 pub env_prefix: String,
297
298 pub base_name: String,
300
301 pub cache_enabled: bool,
303
304 pub search_paths: Vec<PathBuf>,
306
307 pub remote_timeout_secs: u64,
309}
310
311impl LoadOptions {
312 #[must_use]
314 pub fn new() -> Self {
315 Self::default()
316 }
317
318 #[must_use]
320 pub fn builder() -> LoadOptionsBuilder {
321 LoadOptionsBuilder::new()
322 }
323
324 #[must_use]
326 pub const fn is_cache_enabled(&self) -> bool {
327 self.cache_enabled
328 }
329
330 #[must_use]
332 pub const fn is_fail_fast(&self) -> bool {
333 self.fail_fast
334 }
335}
336
337impl Default for LoadOptions {
338 fn default() -> Self {
339 Self {
340 merge_strategy: MergeStrategy::default(),
341 fail_fast: true,
342 ignore_optional_missing: true,
343 validate: true,
344 max_depth: MAX_SEARCH_DEPTH,
345 extensions: DEFAULT_EXTENSIONS.iter().map(|s| s.to_string()).collect(),
346 env_prefix: DEFAULT_ENV_PREFIX.to_string(),
347 base_name: DEFAULT_CONFIG_BASE_NAME.to_string(),
348 cache_enabled: true,
349 search_paths: Vec::new(),
350 remote_timeout_secs: crate::constants::DEFAULT_REMOTE_TIMEOUT_SECS,
351 }
352 }
353}
354
355#[derive(Debug, Clone, Default)]
357pub struct LoadOptionsBuilder {
358 merge_strategy: Option<MergeStrategy>,
359 fail_fast: Option<bool>,
360 ignore_optional_missing: Option<bool>,
361 validate: Option<bool>,
362 max_depth: Option<usize>,
363 extensions: Option<Vec<String>>,
364 env_prefix: Option<String>,
365 base_name: Option<String>,
366 cache_enabled: Option<bool>,
367 search_paths: Option<Vec<PathBuf>>,
368 remote_timeout_secs: Option<u64>,
369}
370
371impl LoadOptionsBuilder {
372 #[must_use]
374 pub fn new() -> Self {
375 Self::default()
376 }
377
378 #[must_use]
380 pub fn merge_strategy(mut self, strategy: MergeStrategy) -> Self {
381 self.merge_strategy = Some(strategy);
382 self
383 }
384
385 #[must_use]
387 pub fn fail_fast(mut self, fail_fast: bool) -> Self {
388 self.fail_fast = Some(fail_fast);
389 self
390 }
391
392 #[must_use]
394 pub fn ignore_optional_missing(mut self, ignore: bool) -> Self {
395 self.ignore_optional_missing = Some(ignore);
396 self
397 }
398
399 #[must_use]
401 pub fn validate(mut self, validate: bool) -> Self {
402 self.validate = Some(validate);
403 self
404 }
405
406 #[must_use]
408 pub fn max_depth(mut self, depth: usize) -> Self {
409 self.max_depth = Some(depth);
410 self
411 }
412
413 #[must_use]
415 pub fn extensions(mut self, extensions: Vec<String>) -> Self {
416 self.extensions = Some(extensions);
417 self
418 }
419
420 #[must_use]
422 pub fn extension(mut self, ext: impl Into<String>) -> Self {
423 self.extensions
424 .get_or_insert_with(Vec::new)
425 .push(ext.into());
426 self
427 }
428
429 #[must_use]
431 pub fn env_prefix(mut self, prefix: impl Into<String>) -> Self {
432 self.env_prefix = Some(prefix.into());
433 self
434 }
435
436 #[must_use]
438 pub fn base_name(mut self, name: impl Into<String>) -> Self {
439 self.base_name = Some(name.into());
440 self
441 }
442
443 #[must_use]
445 pub fn cache_enabled(mut self, enabled: bool) -> Self {
446 self.cache_enabled = Some(enabled);
447 self
448 }
449
450 #[must_use]
452 pub fn search_paths(mut self, paths: Vec<PathBuf>) -> Self {
453 self.search_paths = Some(paths);
454 self
455 }
456
457 #[must_use]
459 pub fn search_path(mut self, path: impl Into<PathBuf>) -> Self {
460 self.search_paths
461 .get_or_insert_with(Vec::new)
462 .push(path.into());
463 self
464 }
465
466 #[must_use]
468 pub fn remote_timeout_secs(mut self, secs: u64) -> Self {
469 self.remote_timeout_secs = Some(secs);
470 self
471 }
472
473 #[must_use]
475 pub fn build(self) -> LoadOptions {
476 let defaults = LoadOptions::default();
477
478 LoadOptions {
479 merge_strategy: self.merge_strategy.unwrap_or(defaults.merge_strategy),
480 fail_fast: self.fail_fast.unwrap_or(defaults.fail_fast),
481 ignore_optional_missing: self
482 .ignore_optional_missing
483 .unwrap_or(defaults.ignore_optional_missing),
484 validate: self.validate.unwrap_or(defaults.validate),
485 max_depth: self.max_depth.unwrap_or(defaults.max_depth),
486 extensions: self.extensions.unwrap_or(defaults.extensions),
487 env_prefix: self.env_prefix.unwrap_or(defaults.env_prefix),
488 base_name: self.base_name.unwrap_or(defaults.base_name),
489 cache_enabled: self.cache_enabled.unwrap_or(defaults.cache_enabled),
490 search_paths: self.search_paths.unwrap_or(defaults.search_paths),
491 remote_timeout_secs: self
492 .remote_timeout_secs
493 .unwrap_or(defaults.remote_timeout_secs),
494 }
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
503 fn test_merge_strategy_default() {
504 let strategy = MergeStrategy::default();
505 assert_eq!(strategy, MergeStrategy::Deep);
506 }
507
508 #[test]
509 fn test_merge_strategy_as_str() {
510 assert_eq!(MergeStrategy::Replace.as_str(), "replace");
511 assert_eq!(MergeStrategy::Deep.as_str(), "deep");
512 assert_eq!(MergeStrategy::Shallow.as_str(), "shallow");
513 assert_eq!(MergeStrategy::Strict.as_str(), "strict");
514 }
515
516 #[test]
517 fn test_merge_strategy_display() {
518 assert_eq!(format!("{}", MergeStrategy::Replace), "replace");
519 }
520
521 #[test]
522 fn test_source_config_new() {
523 let config = SourceConfig::new("test");
524 assert_eq!(config.name, "test");
525 assert!(!config.optional);
526 assert_eq!(config.priority, 0);
527 }
528
529 #[test]
530 fn test_source_config_file() {
531 let config = SourceConfig::file("/etc/config.toml");
532 assert_eq!(config.name, "file");
533 assert_eq!(config.path.unwrap().to_str(), Some("/etc/config.toml"));
534 }
535
536 #[test]
537 fn test_source_config_env() {
538 let config = SourceConfig::env("MYAPP");
539 assert_eq!(config.name, "env");
540 assert_eq!(config.env_prefix.unwrap(), "MYAPP");
541 }
542
543 #[test]
544 fn test_source_config_remote() {
545 let config = SourceConfig::remote("https://example.com/config.json");
546 assert_eq!(config.name, "remote");
547 assert_eq!(config.url.unwrap(), "https://example.com/config.json");
548 }
549
550 #[test]
551 fn test_source_config_builder() {
552 let config = SourceConfig::builder()
553 .name("custom")
554 .path("/path/to/config.toml")
555 .optional(true)
556 .priority(10)
557 .format("toml")
558 .build();
559
560 assert_eq!(config.name, "custom");
561 assert!(config.optional);
562 assert_eq!(config.priority, 10);
563 assert_eq!(config.format.unwrap(), "toml");
564 }
565
566 #[test]
567 fn test_source_config_display_id() {
568 let config = SourceConfig::file("/etc/config.toml");
569 assert_eq!(config.display_id(), "file:/etc/config.toml");
570
571 let config = SourceConfig::env("APP");
572 assert_eq!(config.display_id(), "env:APP");
573 }
574
575 #[test]
576 fn test_source_config_serialization() {
577 let config = SourceConfig::builder().name("test").optional(true).build();
578
579 let json = serde_json::to_string(&config).unwrap();
580 let decoded: SourceConfig = serde_json::from_str(&json).unwrap();
581 assert_eq!(config, decoded);
582 }
583
584 #[test]
585 fn test_load_options_default() {
586 let options = LoadOptions::default();
587 assert_eq!(options.merge_strategy, MergeStrategy::Deep);
588 assert!(options.fail_fast);
589 assert!(options.validate);
590 assert!(options.cache_enabled);
591 }
592
593 #[test]
594 fn test_load_options_builder() {
595 let options = LoadOptions::builder()
596 .merge_strategy(MergeStrategy::Replace)
597 .fail_fast(false)
598 .validate(false)
599 .max_depth(5)
600 .env_prefix("MYAPP")
601 .base_name("settings")
602 .cache_enabled(false)
603 .extension("yaml")
604 .search_path("/etc/myapp")
605 .remote_timeout_secs(60)
606 .build();
607
608 assert_eq!(options.merge_strategy, MergeStrategy::Replace);
609 assert!(!options.fail_fast);
610 assert!(!options.validate);
611 assert_eq!(options.max_depth, 5);
612 assert_eq!(options.env_prefix, "MYAPP");
613 assert_eq!(options.base_name, "settings");
614 assert!(!options.cache_enabled);
615 assert!(options.extensions.contains(&"yaml".to_string()));
616 assert!(options.search_paths.contains(&PathBuf::from("/etc/myapp")));
617 assert_eq!(options.remote_timeout_secs, 60);
618 }
619
620 #[test]
621 fn test_load_options_is_cache_enabled() {
622 let options = LoadOptions::builder().cache_enabled(true).build();
623 assert!(options.is_cache_enabled());
624
625 let options = LoadOptions::builder().cache_enabled(false).build();
626 assert!(!options.is_cache_enabled());
627 }
628
629 #[test]
630 fn test_load_options_is_fail_fast() {
631 let options = LoadOptions::builder().fail_fast(true).build();
632 assert!(options.is_fail_fast());
633
634 let options = LoadOptions::builder().fail_fast(false).build();
635 assert!(!options.is_fail_fast());
636 }
637
638 #[test]
639 fn test_load_options_serialization() {
640 let options = LoadOptions::builder()
641 .merge_strategy(MergeStrategy::Shallow)
642 .fail_fast(false)
643 .build();
644
645 let json = serde_json::to_string(&options).unwrap();
646 let decoded: LoadOptions = serde_json::from_str(&json).unwrap();
647 assert_eq!(options, decoded);
648 }
649}