support_kit/config/
config_sources.rs1use bon::builder;
2use figment::{Figment, Provider};
3use std::fmt::Debug;
4use std::path::PathBuf;
5
6use crate::{Configuration, Environment};
7
8use super::{ConfigDefinition, ConfigFile, ConfigFormat, ConfigManifest};
9
10#[derive(Clone, Debug, bon::Builder)]
11#[builder(derive(Clone))]
12pub struct ConfigSources {
13 #[builder(default, into)]
14 file: ConfigFile,
15 env: Option<Environment>,
16}
17
18impl ConfigSources {
19 pub fn manifest(&self) -> ConfigManifest {
20 let mut definitions = Vec::new();
21 let definition = ConfigDefinition::builder()
22 .maybe_env(self.env)
23 .file(self.file.clone());
24
25 for path in canonical_paths() {
26 let file_definition = definition.clone().path(path);
27
28 definitions.push(file_definition.clone().format(ConfigFormat::Yaml).build());
29 definitions.push(file_definition.clone().format(ConfigFormat::Json).build());
30 definitions.push(file_definition.clone().format(ConfigFormat::Toml).build());
31 }
32
33 definitions.push(definition.env_var((self.file.clone(), self.env)).build());
34
35 ConfigManifest::builder().definitions(definitions).build()
36 }
37
38 pub fn sources(&self) -> figment::Result<ConfigManifest> {
39 let manifest_builder = Self::builder().file(self.file.clone());
40 let mut root_manifest = manifest_builder.clone().build().manifest();
41
42 let next_env = match self.env {
43 Some(env) => Some(env),
44 None => {
45 let figment = Figment::new().merge(&root_manifest);
46
47 figment.extract::<Configuration>()?.environment
48 }
49 };
50
51 let env_manifest = manifest_builder.env(next_env.unwrap_or_default());
52
53 root_manifest.merge(env_manifest.build().manifest());
54
55 Ok(root_manifest)
56 }
57}
58
59impl Provider for ConfigSources {
60 fn metadata(&self) -> figment::Metadata {
61 Default::default()
62 }
63
64 fn data(
65 &self,
66 ) -> Result<figment::value::Map<figment::Profile, figment::value::Dict>, figment::Error> {
67 let sources = self.sources()?;
68
69 Ok(Figment::new().merge(sources).data()?)
70 }
71}
72
73pub fn canonical_paths() -> Vec<PathBuf> {
74 let mut paths = vec![PathBuf::new()];
75
76 paths.extend(dirs::config_dir());
77 paths.extend(dirs::home_dir());
78 paths.reverse();
79
80 paths
81}
82
83#[test]
84fn basic_manifest_matches() {
85 for format in ConfigFormat::all() {
86 figment::Jail::expect_with(|jail| {
87 jail.create_file(format!("support-kit.{format}"), "")?;
88 jail.create_file(format!("support-kit.production.{format}"), "")?;
89
90 let sources = ConfigSources::builder().file("support-kit").build();
91
92 assert_eq!(
93 sources.manifest().known(),
94 ConfigManifest::builder()
95 .definitions(bon::vec![
96 ConfigDefinition::builder()
97 .file("support-kit")
98 .format(format)
99 .build(),
100 ConfigDefinition::builder().env_var("support-kit").build(),
101 ])
102 .build()
103 );
104
105 Ok(())
106 });
107 }
108}
109
110#[test]
111fn env_specific_manifest_matches() {
112 for env in Environment::all() {
113 for format in ConfigFormat::all() {
114 figment::Jail::expect_with(|jail| {
115 jail.create_file(format!("support-kit.{format}"), "")?;
116 jail.create_file(format!("support-kit.{env}.{format}"), "")?;
117
118 let sources = ConfigSources::builder()
119 .file("support-kit")
120 .env(env)
121 .build();
122
123 assert_eq!(
124 sources.manifest().known(),
125 ConfigManifest::builder()
126 .definitions(bon::vec![
127 ConfigDefinition::builder().format(format).env(env).build(),
128 ConfigDefinition::builder()
129 .env_var(("support-kit", env))
130 .build(),
131 ])
132 .build()
133 );
134
135 Ok(())
136 });
137 }
138 }
139}