Skip to main content

kellnr_settings/
settings.rs

1use std::path::{Path, PathBuf};
2
3/// Error returned by settings construction. Aliases [`provcfg::Error`],
4/// every failure surfaces from there (source deserialization or
5/// config-file I/O), so kellnr-side code keeps a domain-flavoured name
6/// without inventing its own opaque box.
7pub use provcfg::Error as SettingsError;
8use provcfg::{ClapArgs, Configurable, Provenance};
9use serde::{Deserialize, Serialize};
10
11use crate::config_source::SourceMap;
12use crate::docs::{Docs, DocsArgs, DocsPartial, DocsProv};
13use crate::gcs::{Gcs, GcsArgs, GcsPartial, GcsProv};
14use crate::local::{Local, LocalArgs, LocalPartial, LocalProv};
15use crate::log::{Log, LogArgs, LogPartial, LogProv};
16use crate::oauth2::{OAuth2, OAuth2Args, OAuth2Partial, OAuth2Prov};
17use crate::origin::{Origin, OriginArgs, OriginPartial, OriginProv};
18use crate::postgresql::{Postgresql, PostgresqlArgs, PostgresqlPartial, PostgresqlProv};
19use crate::proxy::{Proxy, ProxyArgs, ProxyPartial, ProxyProv};
20use crate::registry::{Registry, RegistryArgs, RegistryPartial, RegistryProv};
21use crate::s3::{S3, S3Args, S3Partial, S3Prov};
22use crate::setup::{Setup, SetupArgs, SetupPartial, SetupProv};
23use crate::toolchain::{Toolchain, ToolchainArgs, ToolchainPartial, ToolchainProv};
24
25/// Pure configuration values, every leaf carries the active value, no
26/// provenance attached. Provenance lives on the companion `SettingsProv`,
27/// which is built by [`build_prov_with_cli`] and stored in the app state for
28/// the one handler (`/settings`) that needs it.
29#[derive(Debug, Default, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs)]
30#[serde(default)]
31pub struct Settings {
32    #[configurable(nested)]
33    pub setup: Setup,
34    #[configurable(nested)]
35    pub registry: Registry,
36    #[configurable(nested)]
37    pub docs: Docs,
38    #[configurable(nested)]
39    pub proxy: Proxy,
40    #[configurable(nested)]
41    pub log: Log,
42    #[configurable(nested)]
43    pub local: Local,
44    #[configurable(nested)]
45    pub origin: Origin,
46    #[configurable(nested)]
47    pub postgresql: Postgresql,
48    #[configurable(nested)]
49    pub s3: S3,
50    #[configurable(nested)]
51    pub gcs: Gcs,
52    #[configurable(nested)]
53    pub oauth2: OAuth2,
54    #[configurable(nested)]
55    pub toolchain: Toolchain,
56}
57
58/// Build a `SettingsProv` from the configured sources: optional TOML file,
59/// `KELLNR_*` environment variables, and optionally a CLI partial.
60///
61/// Source order matters, later sources override earlier ones (TOML < env < CLI).
62///
63/// Exposed publicly (in addition to being called by [`crate::cli::parse_cli`])
64/// so consumers like the `kellnr config show` printer can render per-leaf
65/// provenance directly without duplicating the source-stack wiring.
66pub fn build_prov_with_cli(
67    config_file: Option<&Path>,
68    cli_partial: Option<SettingsPartial>,
69) -> Result<SettingsProv, SettingsError> {
70    let mut cfg = provcfg::Config::new();
71    if let Some(path) = config_file {
72        cfg = cfg.add_toml_file(path)?;
73    }
74    cfg = cfg.add_env_with_list_keys("KELLNR", env_list_keys().iter().copied());
75    if let Some(partial) = cli_partial {
76        cfg = cfg.add_cli(partial);
77    }
78    cfg.build::<SettingsProv>()
79}
80
81/// Dotted paths that the env source should treat as comma-separated lists.
82/// Matches the kellnr 6.x behaviour driven by the `config` crate's
83/// `with_list_parse_key`.
84pub(crate) fn env_list_keys() -> &'static [&'static str] {
85    &["registry.required_crate_fields", "oauth2.scopes"]
86}
87
88/// Convert provcfg's per-leaf provenance map into kellnr's `SourceMap`.
89/// Called by the `/settings` web-ui handler to build the API response.
90pub fn sources_from_prov(prov: &SettingsProv) -> SourceMap {
91    prov.sources_map()
92        .into_iter()
93        .map(|(key, category)| (key, category.into()))
94        .collect()
95}
96
97impl Settings {
98    pub fn bin_path(&self) -> PathBuf {
99        PathBuf::from(&self.registry.data_dir).join("crates")
100    }
101
102    pub fn doc_queue_path(&self) -> PathBuf {
103        PathBuf::from(&self.registry.data_dir).join("doc_queue")
104    }
105
106    pub fn sqlite_path(&self) -> PathBuf {
107        PathBuf::from(&self.registry.data_dir).join("db.sqlite")
108    }
109
110    pub fn docs_path(&self) -> PathBuf {
111        PathBuf::from(&self.registry.data_dir).join("docs")
112    }
113
114    pub fn base_path(&self) -> PathBuf {
115        PathBuf::from(&self.registry.data_dir).join("git")
116    }
117
118    pub fn crates_io_bin_path(&self) -> PathBuf {
119        PathBuf::from(&self.registry.data_dir).join("cratesio")
120    }
121
122    pub fn crates_io_path(&self) -> String {
123        format!("{}/cratesio", self.registry.data_dir)
124    }
125
126    pub fn crates_path(&self) -> String {
127        format!("{}/crates", self.registry.data_dir)
128    }
129
130    pub fn crates_path_or_bucket(&self) -> String {
131        if self.s3.enabled {
132            self.s3.crates_bucket.clone()
133        } else if self.gcs.enabled {
134            self.gcs.crates_bucket.clone()
135        } else {
136            self.crates_path()
137        }
138    }
139
140    pub fn crates_io_path_or_bucket(&self) -> String {
141        if self.s3.enabled {
142            self.s3.cratesio_bucket.clone()
143        } else if self.gcs.enabled {
144            self.gcs.cratesio_bucket.clone()
145        } else {
146            self.crates_io_path()
147        }
148    }
149
150    pub fn toolchain_path(&self) -> String {
151        format!("{}/toolchains", self.registry.data_dir)
152    }
153
154    pub fn toolchain_path_or_bucket(&self) -> String {
155        if self.s3.enabled {
156            self.s3.toolchain_bucket.clone()
157        } else if self.gcs.enabled {
158            self.gcs.toolchain_bucket.clone()
159        } else {
160            self.toolchain_path()
161        }
162    }
163}
164
165/// Used in unit and integration tests to provide test settings.
166pub fn test_settings() -> Settings {
167    Settings {
168        registry: Registry {
169            data_dir: "/tmp/kdata_test".to_string(),
170            ..Registry::default()
171        },
172        ..Settings::default()
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use provcfg::Config;
179
180    use super::*;
181    use crate::config_source::ConfigSource;
182
183    #[test]
184    fn sources_from_prov_with_no_sources_marks_every_leaf_default() {
185        let prov = Config::new().build::<SettingsProv>().unwrap();
186        let map = sources_from_prov(&prov);
187
188        // Spot-check a flat leaf and a nested one, every entry must be Default.
189        assert_eq!(map.get("registry.data_dir"), Some(&ConfigSource::Default));
190        assert_eq!(map.get("docs.enabled"), Some(&ConfigSource::Default));
191        assert_eq!(map.get("s3.enabled"), Some(&ConfigSource::Default));
192        assert!(
193            map.values().all(|s| *s == ConfigSource::Default),
194            "every entry must report Default when no sources were added"
195        );
196    }
197
198    #[test]
199    fn sources_from_prov_attributes_toml_leaves_to_toml_source() {
200        let prov = Config::new()
201            .add_toml_str(
202                "test.toml",
203                "[registry]\ndata_dir = \"/tmp/x\"\n[docs]\nenabled = true\n",
204            )
205            .build::<SettingsProv>()
206            .unwrap();
207        let map = sources_from_prov(&prov);
208
209        assert_eq!(map.get("registry.data_dir"), Some(&ConfigSource::Toml));
210        assert_eq!(map.get("docs.enabled"), Some(&ConfigSource::Toml));
211        // Untouched leaves keep Default, File → Toml mapping must not bleed
212        // across leaves that the TOML source didn't supply.
213        assert_eq!(map.get("registry.cache_size"), Some(&ConfigSource::Default));
214        assert_eq!(map.get("s3.enabled"), Some(&ConfigSource::Default));
215    }
216}