1use std::path::{Path, PathBuf};
2
3pub use provcfg::Error as SettingsError;
8use provcfg::{ClapArgs, Configurable, Provenance};
9use serde::{Deserialize, Serialize};
10use utoipa::ToSchema;
11
12use crate::config_source::SourceMap;
13use crate::docs::{Docs, DocsArgs, DocsPartial, DocsProv};
14use crate::filter::{
15 CratesioFilter, CratesioFilterArgs, CratesioFilterPartial, CratesioFilterProv, RegistryFilter,
16 RegistryFilterArgs, RegistryFilterPartial, RegistryFilterProv,
17};
18use crate::gcs::{Gcs, GcsArgs, GcsPartial, GcsProv};
19use crate::local::{Local, LocalArgs, LocalPartial, LocalProv};
20use crate::log::{Log, LogArgs, LogPartial, LogProv};
21use crate::oauth2::{OAuth2, OAuth2Args, OAuth2Partial, OAuth2Prov};
22use crate::origin::{Origin, OriginArgs, OriginPartial, OriginProv};
23use crate::postgresql::{Postgresql, PostgresqlArgs, PostgresqlPartial, PostgresqlProv};
24use crate::proxy::{Proxy, ProxyArgs, ProxyPartial, ProxyProv};
25use crate::registry::{Registry, RegistryArgs, RegistryPartial, RegistryProv};
26use crate::s3::{S3, S3Args, S3Partial, S3Prov};
27use crate::setup::{Setup, SetupArgs, SetupPartial, SetupProv};
28use crate::toolchain::{Toolchain, ToolchainArgs, ToolchainPartial, ToolchainProv};
29use crate::webhooks::{Webhooks, WebhooksArgs, WebhooksPartial, WebhooksProv};
30
31#[derive(
36 Debug, Default, Deserialize, Serialize, Eq, PartialEq, Clone, Configurable, ClapArgs, ToSchema,
37)]
38#[serde(default)]
39pub struct Settings {
40 #[configurable(nested)]
41 pub setup: Setup,
42 #[configurable(nested)]
43 pub registry: Registry,
44 #[configurable(nested)]
45 pub docs: Docs,
46 #[configurable(nested)]
47 pub proxy: Proxy,
48 #[configurable(nested)]
49 pub log: Log,
50 #[configurable(nested)]
51 pub local: Local,
52 #[configurable(nested)]
53 pub origin: Origin,
54 #[configurable(nested)]
55 pub postgresql: Postgresql,
56 #[configurable(nested)]
57 pub s3: S3,
58 #[configurable(nested)]
59 pub gcs: Gcs,
60 #[configurable(nested)]
61 pub oauth2: OAuth2,
62 #[configurable(nested)]
63 pub toolchain: Toolchain,
64 #[configurable(nested)]
65 pub webhooks: Webhooks,
66 #[configurable(nested)]
67 pub cratesio_filter: CratesioFilter,
68 #[configurable(nested)]
69 pub registry_filter: RegistryFilter,
70}
71
72pub fn build_prov_with_cli(
81 config_file: Option<&Path>,
82 cli_partial: Option<SettingsPartial>,
83) -> Result<SettingsProv, SettingsError> {
84 let mut cfg = provcfg::Config::new();
85 if let Some(path) = config_file {
86 cfg = cfg.add_toml_file(path)?;
87 }
88 cfg = cfg.add_env_with_list_keys("KELLNR", env_list_keys().iter().copied());
89 if let Some(partial) = cli_partial {
90 cfg = cfg.add_cli(partial);
91 }
92 cfg.build::<SettingsProv>()
93}
94
95pub fn settings_from_prov(prov: &SettingsProv) -> Settings {
98 let mut settings = Settings::from(prov);
99 settings.normalize();
100 settings
101}
102
103pub(crate) fn env_list_keys() -> &'static [&'static str] {
107 &[
108 "registry.required_crate_fields",
109 "oauth2.scopes",
110 "oauth2.additional_audiences",
111 "docs.build_wrapper",
112 "cratesio_filter.license_allowed",
113 "cratesio_filter.license_exempt_crates",
114 "cratesio_filter.min_release_age_exempt_crates",
115 "registry_filter.license_allowed",
116 "registry_filter.license_exempt_crates",
117 ]
118}
119
120pub fn sources_from_prov(prov: &SettingsProv) -> SourceMap {
123 prov.sources_map()
124 .into_iter()
125 .map(|(key, category)| (key, category.into()))
126 .collect()
127}
128
129impl Settings {
130 pub fn normalize(&mut self) {
132 self.origin.normalize();
133 self.proxy.normalize();
134 }
135
136 pub fn bin_path(&self) -> PathBuf {
137 PathBuf::from(&self.registry.data_dir).join("crates")
138 }
139
140 pub fn doc_queue_path(&self) -> PathBuf {
141 PathBuf::from(&self.registry.data_dir).join("doc_queue")
142 }
143
144 pub fn sqlite_path(&self) -> PathBuf {
145 PathBuf::from(&self.registry.data_dir).join("db.sqlite")
146 }
147
148 pub fn docs_path(&self) -> String {
149 format!("{}/docs", self.registry.data_dir)
150 }
151
152 pub fn base_path(&self) -> PathBuf {
153 PathBuf::from(&self.registry.data_dir).join("git")
154 }
155
156 pub fn crates_io_bin_path(&self) -> PathBuf {
157 PathBuf::from(&self.registry.data_dir).join("cratesio")
158 }
159
160 pub fn crates_io_path(&self) -> String {
161 format!("{}/cratesio", self.registry.data_dir)
162 }
163
164 pub fn crates_path(&self) -> String {
165 format!("{}/crates", self.registry.data_dir)
166 }
167
168 pub fn crates_path_or_bucket(&self) -> String {
169 if self.s3.enabled {
170 self.s3.crates_bucket.clone()
171 } else if self.gcs.enabled {
172 self.gcs.crates_bucket.clone()
173 } else {
174 self.crates_path()
175 }
176 }
177
178 pub fn crates_io_path_or_bucket(&self) -> String {
179 if self.s3.enabled {
180 self.s3.cratesio_bucket.clone()
181 } else if self.gcs.enabled {
182 self.gcs.cratesio_bucket.clone()
183 } else {
184 self.crates_io_path()
185 }
186 }
187
188 pub fn toolchain_path(&self) -> String {
189 format!("{}/toolchains", self.registry.data_dir)
190 }
191
192 pub fn toolchain_path_or_bucket(&self) -> String {
193 if self.s3.enabled {
194 self.s3.toolchain_bucket.clone()
195 } else if self.gcs.enabled {
196 self.gcs.toolchain_bucket.clone()
197 } else {
198 self.toolchain_path()
199 }
200 }
201
202 pub fn docs_backend_is_local(&self) -> bool {
207 !(self.s3.enabled && self.s3.docs_bucket.is_some()
208 || self.gcs.enabled && self.gcs.docs_bucket.is_some())
209 }
210}
211
212#[cfg(any(test, feature = "test-support"))]
217pub fn test_settings() -> Settings {
218 test_settings_in(Path::new("/dev/null/kellnr-test"))
219}
220
221#[cfg(any(test, feature = "test-support"))]
223pub fn test_settings_in(data_dir: &Path) -> Settings {
224 Settings {
225 registry: Registry {
226 data_dir: data_dir.to_string_lossy().into_owned(),
227 ..Registry::default()
228 },
229 ..Settings::default()
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use provcfg::Config;
236
237 use super::*;
238 use crate::config_source::ConfigSource;
239
240 #[test]
241 fn sources_from_prov_with_no_sources_marks_every_leaf_default() {
242 let prov = Config::new().build::<SettingsProv>().unwrap();
243 let map = sources_from_prov(&prov);
244
245 assert_eq!(map.get("registry.data_dir"), Some(&ConfigSource::Default));
247 assert_eq!(map.get("docs.enabled"), Some(&ConfigSource::Default));
248 assert_eq!(map.get("s3.enabled"), Some(&ConfigSource::Default));
249 assert!(
250 map.values().all(|s| *s == ConfigSource::Default),
251 "every entry must report Default when no sources were added"
252 );
253 }
254
255 #[test]
256 fn sources_from_prov_attributes_toml_leaves_to_toml_source() {
257 let prov = Config::new()
258 .add_toml_str(
259 "test.toml",
260 "[registry]\ndata_dir = \"/tmp/x\"\n[docs]\nenabled = true\n",
261 )
262 .build::<SettingsProv>()
263 .unwrap();
264 let map = sources_from_prov(&prov);
265
266 assert_eq!(map.get("registry.data_dir"), Some(&ConfigSource::Toml));
267 assert_eq!(map.get("docs.enabled"), Some(&ConfigSource::Toml));
268 assert_eq!(map.get("registry.cache_size"), Some(&ConfigSource::Default));
271 assert_eq!(map.get("s3.enabled"), Some(&ConfigSource::Default));
272 }
273
274 #[test]
275 fn settings_from_prov_normalizes_origin_path() {
276 let prov = Config::new()
277 .add_toml_str("test.toml", "[origin]\npath = \"registry/\"\n")
278 .build::<SettingsProv>()
279 .unwrap();
280
281 assert_eq!(settings_from_prov(&prov).origin.path, "/registry");
282 }
283
284 #[test]
285 fn settings_from_prov_normalizes_proxy_urls() {
286 let prov = Config::new()
287 .add_toml_str(
288 "test.toml",
289 "[proxy]\nurl = \"https://mirror.example.com/crates\"\n",
290 )
291 .build::<SettingsProv>()
292 .unwrap();
293
294 assert_eq!(
295 settings_from_prov(&prov).proxy.url.as_str(),
296 "https://mirror.example.com/crates/"
297 );
298 }
299}