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 "registry_filter.license_allowed",
115 "registry_filter.license_exempt_crates",
116 ]
117}
118
119pub fn sources_from_prov(prov: &SettingsProv) -> SourceMap {
122 prov.sources_map()
123 .into_iter()
124 .map(|(key, category)| (key, category.into()))
125 .collect()
126}
127
128impl Settings {
129 pub fn normalize(&mut self) {
131 self.origin.normalize();
132 self.proxy.normalize();
133 }
134
135 pub fn bin_path(&self) -> PathBuf {
136 PathBuf::from(&self.registry.data_dir).join("crates")
137 }
138
139 pub fn doc_queue_path(&self) -> PathBuf {
140 PathBuf::from(&self.registry.data_dir).join("doc_queue")
141 }
142
143 pub fn sqlite_path(&self) -> PathBuf {
144 PathBuf::from(&self.registry.data_dir).join("db.sqlite")
145 }
146
147 pub fn docs_path(&self) -> String {
148 format!("{}/docs", self.registry.data_dir)
149 }
150
151 pub fn base_path(&self) -> PathBuf {
152 PathBuf::from(&self.registry.data_dir).join("git")
153 }
154
155 pub fn crates_io_bin_path(&self) -> PathBuf {
156 PathBuf::from(&self.registry.data_dir).join("cratesio")
157 }
158
159 pub fn crates_io_path(&self) -> String {
160 format!("{}/cratesio", self.registry.data_dir)
161 }
162
163 pub fn crates_path(&self) -> String {
164 format!("{}/crates", self.registry.data_dir)
165 }
166
167 pub fn crates_path_or_bucket(&self) -> String {
168 if self.s3.enabled {
169 self.s3.crates_bucket.clone()
170 } else if self.gcs.enabled {
171 self.gcs.crates_bucket.clone()
172 } else {
173 self.crates_path()
174 }
175 }
176
177 pub fn crates_io_path_or_bucket(&self) -> String {
178 if self.s3.enabled {
179 self.s3.cratesio_bucket.clone()
180 } else if self.gcs.enabled {
181 self.gcs.cratesio_bucket.clone()
182 } else {
183 self.crates_io_path()
184 }
185 }
186
187 pub fn toolchain_path(&self) -> String {
188 format!("{}/toolchains", self.registry.data_dir)
189 }
190
191 pub fn toolchain_path_or_bucket(&self) -> String {
192 if self.s3.enabled {
193 self.s3.toolchain_bucket.clone()
194 } else if self.gcs.enabled {
195 self.gcs.toolchain_bucket.clone()
196 } else {
197 self.toolchain_path()
198 }
199 }
200
201 pub fn docs_backend_is_local(&self) -> bool {
206 !(self.s3.enabled && self.s3.docs_bucket.is_some()
207 || self.gcs.enabled && self.gcs.docs_bucket.is_some())
208 }
209}
210
211#[cfg(any(test, feature = "test-support"))]
216pub fn test_settings() -> Settings {
217 test_settings_in(Path::new("/dev/null/kellnr-test"))
218}
219
220#[cfg(any(test, feature = "test-support"))]
222pub fn test_settings_in(data_dir: &Path) -> Settings {
223 Settings {
224 registry: Registry {
225 data_dir: data_dir.to_string_lossy().into_owned(),
226 ..Registry::default()
227 },
228 ..Settings::default()
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use provcfg::Config;
235
236 use super::*;
237 use crate::config_source::ConfigSource;
238
239 #[test]
240 fn sources_from_prov_with_no_sources_marks_every_leaf_default() {
241 let prov = Config::new().build::<SettingsProv>().unwrap();
242 let map = sources_from_prov(&prov);
243
244 assert_eq!(map.get("registry.data_dir"), Some(&ConfigSource::Default));
246 assert_eq!(map.get("docs.enabled"), Some(&ConfigSource::Default));
247 assert_eq!(map.get("s3.enabled"), Some(&ConfigSource::Default));
248 assert!(
249 map.values().all(|s| *s == ConfigSource::Default),
250 "every entry must report Default when no sources were added"
251 );
252 }
253
254 #[test]
255 fn sources_from_prov_attributes_toml_leaves_to_toml_source() {
256 let prov = Config::new()
257 .add_toml_str(
258 "test.toml",
259 "[registry]\ndata_dir = \"/tmp/x\"\n[docs]\nenabled = true\n",
260 )
261 .build::<SettingsProv>()
262 .unwrap();
263 let map = sources_from_prov(&prov);
264
265 assert_eq!(map.get("registry.data_dir"), Some(&ConfigSource::Toml));
266 assert_eq!(map.get("docs.enabled"), Some(&ConfigSource::Toml));
267 assert_eq!(map.get("registry.cache_size"), Some(&ConfigSource::Default));
270 assert_eq!(map.get("s3.enabled"), Some(&ConfigSource::Default));
271 }
272
273 #[test]
274 fn settings_from_prov_normalizes_origin_path() {
275 let prov = Config::new()
276 .add_toml_str("test.toml", "[origin]\npath = \"registry/\"\n")
277 .build::<SettingsProv>()
278 .unwrap();
279
280 assert_eq!(settings_from_prov(&prov).origin.path, "/registry");
281 }
282
283 #[test]
284 fn settings_from_prov_normalizes_proxy_urls() {
285 let prov = Config::new()
286 .add_toml_str(
287 "test.toml",
288 "[proxy]\nurl = \"https://mirror.example.com/crates\"\n",
289 )
290 .build::<SettingsProv>()
291 .unwrap();
292
293 assert_eq!(
294 settings_from_prov(&prov).proxy.url.as_str(),
295 "https://mirror.example.com/crates/"
296 );
297 }
298}