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