1use std::path::PathBuf;
23
24use serde::{Deserialize, Serialize};
25
26use crate::constants::{
27 DEFAULT_CONFIG_BASE_NAME, DEFAULT_ENV_PREFIX, DEFAULT_EXTENSIONS, MAX_SEARCH_DEPTH,
28};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
32#[serde(rename_all = "snake_case")]
33pub enum MergeStrategy {
34 Replace,
36
37 #[default]
39 Deep,
40
41 Shallow,
43
44 Strict,
46}
47
48impl MergeStrategy {
49 #[must_use]
51 pub const fn as_str(&self) -> &'static str {
52 match self {
53 Self::Replace => "replace",
54 Self::Deep => "deep",
55 Self::Shallow => "shallow",
56 Self::Strict => "strict",
57 }
58 }
59}
60
61impl std::fmt::Display for MergeStrategy {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 write!(f, "{}", self.as_str())
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
69#[serde(rename_all = "snake_case")]
70pub enum ErrorMode {
71 #[default]
73 FailFast,
74 CollectAll,
76}
77
78impl ErrorMode {
79 #[must_use]
81 pub const fn from_fail_fast(fail_fast: bool) -> Self {
82 if fail_fast {
83 Self::FailFast
84 } else {
85 Self::CollectAll
86 }
87 }
88
89 #[must_use]
91 pub const fn is_fail_fast(self) -> bool {
92 matches!(self, Self::FailFast)
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
98#[serde(rename_all = "snake_case")]
99pub enum OptionalSourceMode {
100 #[default]
102 IgnoreMissing,
103 RequirePresent,
105}
106
107impl OptionalSourceMode {
108 #[must_use]
110 pub const fn from_ignore_missing(ignore_missing: bool) -> Self {
111 if ignore_missing {
112 Self::IgnoreMissing
113 } else {
114 Self::RequirePresent
115 }
116 }
117
118 #[must_use]
120 pub const fn ignores_missing(self) -> bool {
121 matches!(self, Self::IgnoreMissing)
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
127#[serde(rename_all = "snake_case")]
128pub enum ValidationMode {
129 #[default]
131 Enabled,
132 Disabled,
134}
135
136impl ValidationMode {
137 #[must_use]
139 pub const fn from_enabled(enabled: bool) -> Self {
140 if enabled {
141 Self::Enabled
142 } else {
143 Self::Disabled
144 }
145 }
146
147 #[must_use]
149 pub const fn is_enabled(self) -> bool {
150 matches!(self, Self::Enabled)
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
156#[serde(rename_all = "snake_case")]
157pub enum CacheMode {
158 #[default]
160 Enabled,
161 Disabled,
163}
164
165impl CacheMode {
166 #[must_use]
168 pub const fn from_enabled(enabled: bool) -> Self {
169 if enabled {
170 Self::Enabled
171 } else {
172 Self::Disabled
173 }
174 }
175
176 #[must_use]
178 pub const fn is_enabled(self) -> bool {
179 matches!(self, Self::Enabled)
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct SourceConfig {
188 pub name: String,
190
191 pub optional: bool,
193
194 pub priority: i32,
196
197 pub cache: bool,
199
200 pub format: Option<String>,
202
203 pub path: Option<PathBuf>,
205
206 pub url: Option<String>,
208
209 pub env_prefix: Option<String>,
211
212 pub extra: std::collections::BTreeMap<String, String>,
214}
215
216impl SourceConfig {
217 #[must_use]
219 pub fn new(name: impl Into<String>) -> Self {
220 Self {
221 name: name.into(),
222 optional: false,
223 priority: 0,
224 cache: true,
225 format: None,
226 path: None,
227 url: None,
228 env_prefix: None,
229 extra: std::collections::BTreeMap::new(),
230 }
231 }
232
233 #[must_use]
235 pub fn builder() -> SourceConfigBuilder {
236 SourceConfigBuilder::new()
237 }
238
239 #[must_use]
241 pub fn file(path: impl Into<PathBuf>) -> Self {
242 Self::builder().name("file").path(path).build()
243 }
244
245 #[must_use]
247 pub fn env(prefix: impl Into<String>) -> Self {
248 Self::builder().name("env").env_prefix(prefix).build()
249 }
250
251 #[must_use]
253 pub const fn is_optional(&self) -> bool {
254 self.optional
255 }
256
257 #[must_use]
259 pub fn display_id(&self) -> String {
260 self.path
261 .as_ref()
262 .map(|path| path.display().to_string())
263 .or_else(|| self.url.clone())
264 .or_else(|| self.env_prefix.clone())
265 .map_or_else(
266 || self.name.clone(),
267 |target| format!("{}:{target}", self.name),
268 )
269 }
270}
271
272impl Default for SourceConfig {
273 fn default() -> Self {
274 Self::new(DEFAULT_CONFIG_BASE_NAME)
275 }
276}
277
278#[derive(Debug, Clone, Default)]
280pub struct SourceConfigBuilder {
281 name: Option<String>,
282 optional: bool,
283 priority: i32,
284 cache: bool,
285 format: Option<String>,
286 path: Option<PathBuf>,
287 url: Option<String>,
288 env_prefix: Option<String>,
289 extra: std::collections::BTreeMap<String, String>,
290}
291
292impl SourceConfigBuilder {
293 #[must_use]
295 pub fn new() -> Self {
296 Self {
297 cache: true,
298 ..Self::default()
299 }
300 }
301
302 #[must_use]
304 pub fn name(mut self, name: impl Into<String>) -> Self {
305 self.name = Some(name.into());
306 self
307 }
308
309 #[must_use]
311 pub const fn optional(mut self, optional: bool) -> Self {
312 self.optional = optional;
313 self
314 }
315
316 #[must_use]
318 pub const fn priority(mut self, priority: i32) -> Self {
319 self.priority = priority;
320 self
321 }
322
323 #[must_use]
325 pub const fn cache(mut self, cache: bool) -> Self {
326 self.cache = cache;
327 self
328 }
329
330 #[must_use]
332 pub fn format(mut self, format: impl Into<String>) -> Self {
333 self.format = Some(format.into());
334 self
335 }
336
337 #[must_use]
339 pub fn path(mut self, path: impl Into<PathBuf>) -> Self {
340 self.path = Some(path.into());
341 self
342 }
343
344 #[must_use]
346 pub fn url(mut self, url: impl Into<String>) -> Self {
347 self.url = Some(url.into());
348 self
349 }
350
351 #[must_use]
353 pub fn env_prefix(mut self, prefix: impl Into<String>) -> Self {
354 self.env_prefix = Some(prefix.into());
355 self
356 }
357
358 #[must_use]
360 pub fn extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
361 self.extra.insert(key.into(), value.into());
362 self
363 }
364
365 #[must_use]
367 pub fn build(self) -> SourceConfig {
368 SourceConfig {
369 name: self
370 .name
371 .unwrap_or_else(|| DEFAULT_CONFIG_BASE_NAME.to_string()),
372 optional: self.optional,
373 priority: self.priority,
374 cache: self.cache,
375 format: self.format,
376 path: self.path,
377 url: self.url,
378 env_prefix: self.env_prefix,
379 extra: self.extra,
380 }
381 }
382}
383
384#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388pub struct LoadOptions {
389 pub merge_strategy: MergeStrategy,
391
392 pub error_mode: ErrorMode,
394
395 pub optional_source_mode: OptionalSourceMode,
397
398 pub validation_mode: ValidationMode,
400
401 pub max_depth: usize,
403
404 pub extensions: Vec<String>,
406
407 pub env_prefix: String,
409
410 pub base_name: String,
412
413 pub cache_mode: CacheMode,
415
416 pub search_paths: Vec<PathBuf>,
418}
419
420impl LoadOptions {
421 #[must_use]
423 pub fn new() -> Self {
424 Self::default()
425 }
426
427 #[must_use]
429 pub fn builder() -> LoadOptionsBuilder {
430 LoadOptionsBuilder::new()
431 }
432
433 #[must_use]
435 pub const fn is_cache_enabled(&self) -> bool {
436 self.cache_mode.is_enabled()
437 }
438
439 #[must_use]
441 pub const fn is_fail_fast(&self) -> bool {
442 self.error_mode.is_fail_fast()
443 }
444
445 #[must_use]
447 pub const fn ignores_missing_optional(&self) -> bool {
448 self.optional_source_mode.ignores_missing()
449 }
450
451 #[must_use]
453 pub const fn validates(&self) -> bool {
454 self.validation_mode.is_enabled()
455 }
456}
457
458impl Default for LoadOptions {
459 fn default() -> Self {
460 Self {
461 merge_strategy: MergeStrategy::default(),
462 error_mode: ErrorMode::default(),
463 optional_source_mode: OptionalSourceMode::default(),
464 validation_mode: ValidationMode::default(),
465 max_depth: MAX_SEARCH_DEPTH,
466 extensions: DEFAULT_EXTENSIONS.iter().map(ToString::to_string).collect(),
467 env_prefix: DEFAULT_ENV_PREFIX.to_string(),
468 base_name: DEFAULT_CONFIG_BASE_NAME.to_string(),
469 cache_mode: CacheMode::default(),
470 search_paths: Vec::new(),
471 }
472 }
473}
474
475#[derive(Debug, Clone, Default)]
477pub struct LoadOptionsBuilder {
478 merge_strategy: Option<MergeStrategy>,
479 error_mode: Option<ErrorMode>,
480 optional_source_mode: Option<OptionalSourceMode>,
481 validation_mode: Option<ValidationMode>,
482 max_depth: Option<usize>,
483 extensions: Option<Vec<String>>,
484 env_prefix: Option<String>,
485 base_name: Option<String>,
486 cache_mode: Option<CacheMode>,
487 search_paths: Option<Vec<PathBuf>>,
488}
489
490impl LoadOptionsBuilder {
491 #[must_use]
493 pub fn new() -> Self {
494 Self::default()
495 }
496
497 #[must_use]
499 pub const fn merge_strategy(mut self, strategy: MergeStrategy) -> Self {
500 self.merge_strategy = Some(strategy);
501 self
502 }
503
504 #[must_use]
506 pub const fn fail_fast(mut self, fail_fast: bool) -> Self {
507 self.error_mode = Some(ErrorMode::from_fail_fast(fail_fast));
508 self
509 }
510
511 #[must_use]
513 pub const fn ignore_optional_missing(mut self, ignore: bool) -> Self {
514 self.optional_source_mode = Some(OptionalSourceMode::from_ignore_missing(ignore));
515 self
516 }
517
518 #[must_use]
520 pub const fn validate(mut self, validate: bool) -> Self {
521 self.validation_mode = Some(ValidationMode::from_enabled(validate));
522 self
523 }
524
525 #[must_use]
527 pub const fn max_depth(mut self, depth: usize) -> Self {
528 self.max_depth = Some(depth);
529 self
530 }
531
532 #[must_use]
534 pub fn extensions(mut self, extensions: Vec<String>) -> Self {
535 self.extensions = Some(extensions);
536 self
537 }
538
539 #[must_use]
541 pub fn extension(mut self, ext: impl Into<String>) -> Self {
542 self.extensions
543 .get_or_insert_with(Vec::new)
544 .push(ext.into());
545 self
546 }
547
548 #[must_use]
550 pub fn env_prefix(mut self, prefix: impl Into<String>) -> Self {
551 self.env_prefix = Some(prefix.into());
552 self
553 }
554
555 #[must_use]
557 pub fn base_name(mut self, name: impl Into<String>) -> Self {
558 self.base_name = Some(name.into());
559 self
560 }
561
562 #[must_use]
564 pub const fn cache_enabled(mut self, enabled: bool) -> Self {
565 self.cache_mode = Some(CacheMode::from_enabled(enabled));
566 self
567 }
568
569 #[must_use]
571 pub fn search_paths(mut self, paths: Vec<PathBuf>) -> Self {
572 self.search_paths = Some(paths);
573 self
574 }
575
576 #[must_use]
578 pub fn search_path(mut self, path: impl Into<PathBuf>) -> Self {
579 self.search_paths
580 .get_or_insert_with(Vec::new)
581 .push(path.into());
582 self
583 }
584
585 #[must_use]
587 pub fn build(self) -> LoadOptions {
588 let defaults = LoadOptions::default();
589
590 LoadOptions {
591 merge_strategy: self.merge_strategy.unwrap_or(defaults.merge_strategy),
592 error_mode: self.error_mode.unwrap_or(defaults.error_mode),
593 optional_source_mode: self
594 .optional_source_mode
595 .unwrap_or(defaults.optional_source_mode),
596 validation_mode: self.validation_mode.unwrap_or(defaults.validation_mode),
597 max_depth: self.max_depth.unwrap_or(defaults.max_depth),
598 extensions: self.extensions.unwrap_or(defaults.extensions),
599 env_prefix: self.env_prefix.unwrap_or(defaults.env_prefix),
600 base_name: self.base_name.unwrap_or(defaults.base_name),
601 cache_mode: self.cache_mode.unwrap_or(defaults.cache_mode),
602 search_paths: self.search_paths.unwrap_or(defaults.search_paths),
603 }
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610
611 #[test]
612 fn test_merge_strategy_default() {
613 let strategy = MergeStrategy::default();
614 assert_eq!(strategy, MergeStrategy::Deep);
615 }
616
617 #[test]
618 fn test_merge_strategy_as_str() {
619 assert_eq!(MergeStrategy::Replace.as_str(), "replace");
620 assert_eq!(MergeStrategy::Deep.as_str(), "deep");
621 assert_eq!(MergeStrategy::Shallow.as_str(), "shallow");
622 assert_eq!(MergeStrategy::Strict.as_str(), "strict");
623 }
624
625 #[test]
626 fn test_merge_strategy_display() {
627 assert_eq!(format!("{}", MergeStrategy::Replace), "replace");
628 }
629
630 #[test]
631 fn test_source_config_new() {
632 let config = SourceConfig::new("test");
633 assert_eq!(config.name, "test");
634 assert!(!config.optional);
635 assert_eq!(config.priority, 0);
636 }
637
638 #[test]
639 fn test_source_config_file() {
640 let config = SourceConfig::file("/etc/config.toml");
641 assert_eq!(config.name, "file");
642 assert_eq!(config.path.unwrap().to_str(), Some("/etc/config.toml"));
643 }
644
645 #[test]
646 fn test_source_config_env() {
647 let config = SourceConfig::env("MYAPP");
648 assert_eq!(config.name, "env");
649 assert_eq!(config.env_prefix.unwrap(), "MYAPP");
650 }
651
652 #[test]
653 fn test_source_config_url_builder() {
654 let config = SourceConfig::builder()
655 .name("custom-url")
656 .url("https://example.com/config.json")
657 .build();
658 assert_eq!(config.name, "custom-url");
659 assert_eq!(config.url.unwrap(), "https://example.com/config.json");
660 }
661
662 #[test]
663 fn test_source_config_builder() {
664 let config = SourceConfig::builder()
665 .name("custom")
666 .path("/path/to/config.toml")
667 .optional(true)
668 .priority(10)
669 .format("toml")
670 .build();
671
672 assert_eq!(config.name, "custom");
673 assert!(config.optional);
674 assert_eq!(config.priority, 10);
675 assert_eq!(config.format.unwrap(), "toml");
676 }
677
678 #[test]
679 fn test_source_config_display_id() {
680 let config = SourceConfig::file("/etc/config.toml");
681 assert_eq!(config.display_id(), "file:/etc/config.toml");
682
683 let config = SourceConfig::env("APP");
684 assert_eq!(config.display_id(), "env:APP");
685 }
686
687 #[test]
688 fn test_source_config_serialization() {
689 let config = SourceConfig::builder().name("test").optional(true).build();
690
691 let json = serde_json::to_string(&config).unwrap();
692 let decoded: SourceConfig = serde_json::from_str(&json).unwrap();
693 assert_eq!(config, decoded);
694 }
695
696 #[test]
697 fn test_load_options_default() {
698 let options = LoadOptions::default();
699 assert_eq!(options.merge_strategy, MergeStrategy::Deep);
700 assert!(options.is_fail_fast());
701 assert!(options.validates());
702 assert!(options.is_cache_enabled());
703 }
704
705 #[test]
706 fn test_load_options_builder() {
707 let options = LoadOptions::builder()
708 .merge_strategy(MergeStrategy::Replace)
709 .fail_fast(false)
710 .validate(false)
711 .max_depth(5)
712 .env_prefix("MYAPP")
713 .base_name("settings")
714 .cache_enabled(false)
715 .extension("yaml")
716 .search_path("/etc/myapp")
717 .build();
718
719 assert_eq!(options.merge_strategy, MergeStrategy::Replace);
720 assert!(!options.is_fail_fast());
721 assert!(!options.validates());
722 assert_eq!(options.max_depth, 5);
723 assert_eq!(options.env_prefix, "MYAPP");
724 assert_eq!(options.base_name, "settings");
725 assert!(!options.is_cache_enabled());
726 assert!(options.extensions.contains(&"yaml".to_string()));
727 assert!(options.search_paths.contains(&PathBuf::from("/etc/myapp")));
728 }
729
730 #[test]
731 fn test_load_options_is_cache_enabled() {
732 let options = LoadOptions::builder().cache_enabled(true).build();
733 assert!(options.is_cache_enabled());
734
735 let options = LoadOptions::builder().cache_enabled(false).build();
736 assert!(!options.is_cache_enabled());
737 }
738
739 #[test]
740 fn test_load_options_is_fail_fast() {
741 let options = LoadOptions::builder().fail_fast(true).build();
742 assert!(options.is_fail_fast());
743
744 let options = LoadOptions::builder().fail_fast(false).build();
745 assert!(!options.is_fail_fast());
746 }
747
748 #[test]
749 fn test_load_options_serialization() {
750 let options = LoadOptions::builder()
751 .merge_strategy(MergeStrategy::Shallow)
752 .fail_fast(false)
753 .build();
754
755 let json = serde_json::to_string(&options).unwrap();
756 let decoded: LoadOptions = serde_json::from_str(&json).unwrap();
757 assert_eq!(options, decoded);
758 }
759}