support_kit/config/
config_sources.rs

1use 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}