Skip to main content

cargo/context/
schema.rs

1//! Cargo configuration schemas.
2//!
3//! This module contains types that define the schema for various configuration
4//! sections found in Cargo configuration.
5//!
6//! These types are mostly used by [`GlobalContext::get`](super::GlobalContext::get)
7//! to deserialize configuration values from TOML files, environment variables,
8//! and CLI arguments.
9//!
10//! Schema types here should only contain data and simple accessor methods.
11//! Avoid depending on [`GlobalContext`](super::GlobalContext) directly.
12
13use crate::sources::CRATES_IO_REGISTRY;
14use crate::util::data_structures::HashMap;
15use std::borrow::Cow;
16use std::ffi::OsStr;
17use std::str::FromStr;
18use std::{fmt, hash};
19
20use cargo_credential::Secret;
21use serde::Deserialize;
22use serde::Serialize;
23use serde::de;
24use serde_untagged::UntaggedEnumVisitor;
25
26use std::path::Path;
27
28use crate::CargoResult;
29
30use super::OptValue;
31use super::PathAndArgs;
32use super::StringList;
33use super::Value;
34use super::path::ConfigRelativePath;
35
36#[derive(Debug, Default, Deserialize, PartialEq)]
37#[serde(rename_all = "kebab-case")]
38pub struct CargoCacheConfig {
39    /// How often to automatically clean unused cache data.
40    pub auto_clean_frequency: Option<String>,
41    /// Settings for cleaning the global cache.
42    pub global_clean: Option<GlobalCleanConfig>,
43}
44
45/// Cache cleaning settings from the `cache.global-clean` config table.
46///
47/// NOTE: Not all of these options may get stabilized. Some of them are very
48/// low-level details, and may not be something typical users need.
49///
50/// If any of these options are `None`, the built-in default is used.
51#[derive(Debug, Default, Deserialize, PartialEq)]
52#[serde(rename_all = "kebab-case")]
53pub struct GlobalCleanConfig {
54    /// Anything older than this duration will be deleted in the source cache.
55    pub max_src_age: Option<String>,
56    /// Anything older than this duration will be deleted in the compressed crate cache.
57    pub max_crate_age: Option<String>,
58    /// Any index older than this duration will be deleted from the index cache.
59    pub max_index_age: Option<String>,
60    /// Any git checkout older than this duration will be deleted from the checkout cache.
61    pub max_git_co_age: Option<String>,
62    /// Any git clone older than this duration will be deleted from the git cache.
63    pub max_git_db_age: Option<String>,
64}
65
66#[derive(Deserialize)]
67#[serde(rename_all = "kebab-case")]
68pub struct CargoNewConfig {
69    #[deprecated = "cargo-new no longer supports adding the authors field"]
70    #[expect(dead_code, reason = "deprecated")]
71    name: Option<String>,
72
73    #[deprecated = "cargo-new no longer supports adding the authors field"]
74    #[expect(dead_code, reason = "deprecated")]
75    email: Option<String>,
76
77    #[serde(rename = "vcs")]
78    pub version_control: Option<VersionControl>,
79}
80
81#[derive(Clone, Copy, Debug, PartialEq)]
82pub enum VersionControl {
83    Git,
84    Hg,
85    Pijul,
86    Fossil,
87    NoVcs,
88}
89
90impl VersionControl {
91    pub const VALUES: &[Self] = &[Self::Git, Self::Hg, Self::Pijul, Self::Fossil, Self::NoVcs];
92
93    pub fn as_str(&self) -> &'static str {
94        match self {
95            VersionControl::Git => "git",
96            VersionControl::Hg => "hg",
97            VersionControl::Pijul => "pijul",
98            VersionControl::Fossil => "fossil",
99            VersionControl::NoVcs => "none",
100        }
101    }
102}
103
104impl FromStr for VersionControl {
105    type Err = anyhow::Error;
106
107    fn from_str(s: &str) -> Result<Self, anyhow::Error> {
108        match s {
109            "git" => Ok(VersionControl::Git),
110            "hg" => Ok(VersionControl::Hg),
111            "pijul" => Ok(VersionControl::Pijul),
112            "fossil" => Ok(VersionControl::Fossil),
113            "none" => Ok(VersionControl::NoVcs),
114            other => anyhow::bail!("unknown vcs specification: `{}`", other),
115        }
116    }
117}
118
119impl<'de> de::Deserialize<'de> for VersionControl {
120    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
121    where
122        D: de::Deserializer<'de>,
123    {
124        let s = String::deserialize(deserializer)?;
125        FromStr::from_str(&s).map_err(de::Error::custom)
126    }
127}
128
129/// Definition of a source in a config file.
130#[derive(Debug, Deserialize)]
131#[serde(rename_all = "kebab-case")]
132pub struct SourceConfigDef {
133    /// Indicates this source should be replaced with another of the given name.
134    pub replace_with: OptValue<String>,
135    /// A directory source.
136    pub directory: Option<ConfigRelativePath>,
137    /// A registry source. Value is a URL.
138    pub registry: OptValue<String>,
139    /// A local registry source.
140    pub local_registry: Option<ConfigRelativePath>,
141    /// A git source. Value is a URL.
142    pub git: OptValue<String>,
143    /// The git branch.
144    pub branch: OptValue<String>,
145    /// The git tag.
146    pub tag: OptValue<String>,
147    /// The git revision.
148    pub rev: OptValue<String>,
149}
150
151/// A map of registry names to URLs where documentations are hosted.
152/// This is for unstable feature [`-Zrustdoc-map`][1].
153///
154/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
155#[derive(serde::Deserialize, Debug)]
156#[serde(default)]
157pub struct RustdocExternMap {
158    #[serde(deserialize_with = "default_crates_io_to_docs_rs")]
159    /// * Key is the registry name in the configuration `[registries.<name>]`.
160    /// * Value is the URL where the documentation is hosted.
161    pub registries: HashMap<String, String>,
162    pub std: Option<RustdocExternMode>,
163}
164
165impl Default for RustdocExternMap {
166    fn default() -> Self {
167        Self {
168            registries: HashMap::from_iter([(CRATES_IO_REGISTRY.into(), DOCS_RS_URL.into())]),
169            std: None,
170        }
171    }
172}
173
174const DOCS_RS_URL: &'static str = "https://docs.rs/";
175
176fn default_crates_io_to_docs_rs<'de, D: serde::Deserializer<'de>>(
177    de: D,
178) -> Result<HashMap<String, String>, D::Error> {
179    let mut registries = HashMap::deserialize(de)?;
180    if !registries.contains_key(CRATES_IO_REGISTRY) {
181        registries.insert(CRATES_IO_REGISTRY.into(), DOCS_RS_URL.into());
182    }
183    Ok(registries)
184}
185
186impl hash::Hash for RustdocExternMap {
187    fn hash<H: hash::Hasher>(&self, into: &mut H) {
188        self.std.hash(into);
189        for (key, value) in &self.registries {
190            key.hash(into);
191            value.hash(into);
192        }
193    }
194}
195
196/// Mode used for `std`. This is for unstable feature [`-Zrustdoc-map`][1].
197///
198/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#rustdoc-map
199#[derive(Debug, Hash)]
200pub enum RustdocExternMode {
201    /// Use a local `file://` URL.
202    Local,
203    /// Use a remote URL to <https://doc.rust-lang.org/> (default).
204    Remote,
205    /// An arbitrary URL.
206    Url(String),
207}
208
209impl From<String> for RustdocExternMode {
210    fn from(s: String) -> RustdocExternMode {
211        match s.as_ref() {
212            "local" => RustdocExternMode::Local,
213            "remote" => RustdocExternMode::Remote,
214            _ => RustdocExternMode::Url(s),
215        }
216    }
217}
218
219impl fmt::Display for RustdocExternMode {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        match self {
222            RustdocExternMode::Local => "local".fmt(f),
223            RustdocExternMode::Remote => "remote".fmt(f),
224            RustdocExternMode::Url(s) => s.fmt(f),
225        }
226    }
227}
228
229impl<'de> serde::de::Deserialize<'de> for RustdocExternMode {
230    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
231    where
232        D: serde::de::Deserializer<'de>,
233    {
234        let s = String::deserialize(deserializer)?;
235        Ok(s.into())
236    }
237}
238
239/// The `[http]` table.
240///
241/// Example configuration:
242///
243/// ```toml
244/// [http]
245/// proxy = "host:port"
246/// timeout = 30
247/// cainfo = "/path/to/ca-bundle.crt"
248/// check-revoke = true
249/// multiplexing = true
250/// ssl-version = "tlsv1.3"
251/// ```
252#[derive(Debug, Default, Deserialize, PartialEq)]
253#[serde(rename_all = "kebab-case")]
254pub struct CargoHttpConfig {
255    pub proxy: Option<String>,
256    pub low_speed_limit: Option<u32>,
257    pub timeout: Option<u64>,
258    pub cainfo: Option<ConfigRelativePath>,
259    pub proxy_cainfo: Option<ConfigRelativePath>,
260    pub check_revoke: Option<bool>,
261    pub user_agent: Option<String>,
262    pub debug: Option<bool>,
263    pub multiplexing: Option<bool>,
264    pub ssl_version: Option<SslVersionConfig>,
265}
266
267/// The `[future-incompat-report]` stable
268///
269/// Example configuration:
270///
271/// ```toml
272/// [future-incompat-report]
273/// frequency = "always"
274/// ```
275#[derive(Debug, Default, Deserialize, PartialEq)]
276#[serde(rename_all = "kebab-case")]
277pub struct CargoFutureIncompatConfig {
278    frequency: Option<CargoFutureIncompatFrequencyConfig>,
279}
280
281#[derive(Debug, Default, Deserialize, PartialEq)]
282#[serde(rename_all = "kebab-case")]
283pub enum CargoFutureIncompatFrequencyConfig {
284    #[default]
285    Always,
286    Never,
287}
288
289impl CargoFutureIncompatConfig {
290    pub fn should_display_message(&self) -> bool {
291        use CargoFutureIncompatFrequencyConfig::*;
292
293        let frequency = self.frequency.as_ref().unwrap_or(&Always);
294        match frequency {
295            Always => true,
296            Never => false,
297        }
298    }
299}
300
301/// Configuration for `ssl-version` in `http` section
302/// There are two ways to configure:
303///
304/// ```text
305/// [http]
306/// ssl-version = "tlsv1.3"
307/// ```
308///
309/// ```text
310/// [http]
311/// ssl-version.min = "tlsv1.2"
312/// ssl-version.max = "tlsv1.3"
313/// ```
314#[derive(Clone, Debug, PartialEq)]
315pub enum SslVersionConfig {
316    Single(String),
317    Range(SslVersionConfigRange),
318}
319
320impl<'de> Deserialize<'de> for SslVersionConfig {
321    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
322    where
323        D: serde::Deserializer<'de>,
324    {
325        UntaggedEnumVisitor::new()
326            .string(|single| Ok(SslVersionConfig::Single(single.to_owned())))
327            .map(|map| map.deserialize().map(SslVersionConfig::Range))
328            .deserialize(deserializer)
329    }
330}
331
332#[derive(Clone, Debug, Deserialize, PartialEq)]
333#[serde(rename_all = "kebab-case")]
334pub struct SslVersionConfigRange {
335    pub min: Option<String>,
336    pub max: Option<String>,
337}
338
339/// The `[net]` table.
340///
341/// Example configuration:
342///
343/// ```toml
344/// [net]
345/// retry = 2
346/// offline = false
347/// git-fetch-with-cli = true
348/// ```
349#[derive(Debug, Deserialize)]
350#[serde(rename_all = "kebab-case")]
351pub struct CargoNetConfig {
352    pub retry: Option<u32>,
353    pub offline: Option<bool>,
354    pub git_fetch_with_cli: Option<bool>,
355    pub ssh: Option<CargoSshConfig>,
356}
357
358#[derive(Debug, Deserialize)]
359#[serde(rename_all = "kebab-case")]
360pub struct CargoSshConfig {
361    pub known_hosts: Option<Vec<Value<String>>>,
362}
363
364/// Configuration for `jobs` in `build` section. There are two
365/// ways to configure: An integer or a simple string expression.
366///
367/// ```toml
368/// [build]
369/// jobs = 1
370/// ```
371///
372/// ```toml
373/// [build]
374/// jobs = "default" # Currently only support "default".
375/// ```
376#[derive(Debug, Clone)]
377pub enum JobsConfig {
378    Integer(i32),
379    String(String),
380}
381
382impl<'de> Deserialize<'de> for JobsConfig {
383    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
384    where
385        D: serde::Deserializer<'de>,
386    {
387        UntaggedEnumVisitor::new()
388            .i32(|int| Ok(JobsConfig::Integer(int)))
389            .string(|string| Ok(JobsConfig::String(string.to_owned())))
390            .deserialize(deserializer)
391    }
392}
393
394/// The `[build]` table.
395///
396/// Example configuration:
397///
398/// ```toml
399/// [build]
400/// jobs = 4
401/// target = "x86_64-unknown-linux-gnu"
402/// target-dir = "target"
403/// rustflags = ["-C", "link-arg=-fuse-ld=lld"]
404/// incremental = true
405/// ```
406#[derive(Debug, Deserialize)]
407#[serde(rename_all = "kebab-case")]
408pub struct CargoBuildConfig {
409    // deprecated, but preserved for compatibility
410    pub pipelining: Option<bool>,
411    pub dep_info_basedir: Option<ConfigRelativePath>,
412    pub target_dir: Option<ConfigRelativePath>,
413    pub build_dir: Option<ConfigRelativePath>,
414    pub incremental: Option<bool>,
415    pub target: Option<BuildTargetConfig>,
416    pub jobs: Option<JobsConfig>,
417    pub rustflags: Option<StringList>,
418    pub rustdocflags: Option<StringList>,
419    pub rustc_wrapper: Option<ConfigRelativePath>,
420    pub rustc_workspace_wrapper: Option<ConfigRelativePath>,
421    pub rustc: Option<ConfigRelativePath>,
422    pub rustdoc: Option<ConfigRelativePath>,
423    pub artifact_dir: Option<ConfigRelativePath>,
424    pub warnings: Option<WarningHandling>,
425    /// Unstable feature `-Zsbom`.
426    pub sbom: Option<bool>,
427    /// Unstable feature `-Zbuild-analysis`.
428    pub analysis: Option<CargoBuildAnalysis>,
429    /// Unstable feature `-Zchecksum-freshness`.
430    pub fingerprint: Option<FingerprintMethod>,
431}
432
433/// Metrics collection for build analysis.
434#[derive(Debug, Deserialize, Default)]
435#[serde(rename_all = "kebab-case")]
436pub struct CargoBuildAnalysis {
437    pub enabled: bool,
438}
439
440/// Whether warnings should warn, be allowed, or cause an error.
441#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Default)]
442#[serde(rename_all = "kebab-case")]
443pub enum WarningHandling {
444    #[default]
445    /// Output warnings.
446    Warn,
447    /// Allow warnings (do not output them).
448    Allow,
449    /// Error if  warnings are emitted.
450    Deny,
451}
452
453#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, Default)]
454#[serde(rename_all = "kebab-case")]
455pub enum FingerprintMethod {
456    #[default]
457    Mtime,
458    Content,
459}
460
461impl FingerprintMethod {
462    pub fn as_str(&self) -> &'static str {
463        match self {
464            Self::Mtime => "mtime",
465            Self::Content => "content",
466        }
467    }
468}
469
470impl std::fmt::Display for FingerprintMethod {
471    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472        self.as_str().fmt(f)
473    }
474}
475
476/// Configuration for `build.target`.
477///
478/// Accepts in the following forms:
479///
480/// ```toml
481/// target = "a"
482/// target = ["a"]
483/// target = ["a", "b"]
484/// ```
485#[derive(Debug, Deserialize)]
486#[serde(transparent)]
487pub struct BuildTargetConfig {
488    inner: Value<BuildTargetConfigInner>,
489}
490
491#[derive(Debug)]
492enum BuildTargetConfigInner {
493    One(String),
494    Many(Vec<String>),
495}
496
497impl<'de> Deserialize<'de> for BuildTargetConfigInner {
498    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
499    where
500        D: serde::Deserializer<'de>,
501    {
502        UntaggedEnumVisitor::new()
503            .string(|one| Ok(BuildTargetConfigInner::One(one.to_owned())))
504            .seq(|many| many.deserialize().map(BuildTargetConfigInner::Many))
505            .deserialize(deserializer)
506    }
507}
508
509impl BuildTargetConfig {
510    /// Gets values of `build.target` as a list of strings.
511    pub fn values(&self, cwd: &Path) -> CargoResult<Vec<String>> {
512        let map = |s: &String| {
513            if s.ends_with(".json") {
514                // Path to a target specification file (in JSON).
515                // <https://doc.rust-lang.org/rustc/targets/custom.html>
516                self.inner
517                    .definition
518                    .root(cwd)
519                    .join(s)
520                    .to_str()
521                    .expect("must be utf-8 in toml")
522                    .to_string()
523            } else {
524                // A string. Probably a target tuple.
525                s.to_string()
526            }
527        };
528        let values = match &self.inner.val {
529            BuildTargetConfigInner::One(s) => vec![map(s)],
530            BuildTargetConfigInner::Many(v) => v.iter().map(map).collect(),
531        };
532        Ok(values)
533    }
534}
535
536/// The `[resolver]` table.
537///
538/// Example configuration:
539///
540/// ```toml
541/// [resolver]
542/// incompatible-rust-versions = "fallback"
543/// incompatible-publish-age = "deny"
544/// feature-unification = "workspace"
545/// lockfile-path = "my/Cargo.lock"
546/// ```
547#[derive(Debug, Deserialize)]
548#[serde(rename_all = "kebab-case")]
549pub struct CargoResolverConfig {
550    pub incompatible_rust_versions: Option<IncompatibleRustVersions>,
551    pub incompatible_publish_age: Option<IncompatiblePublishAge>,
552    pub feature_unification: Option<FeatureUnification>,
553    pub lockfile_path: Option<ConfigRelativePath>,
554}
555
556#[derive(Debug, Deserialize, PartialEq, Eq)]
557#[serde(rename_all = "kebab-case")]
558pub enum IncompatibleRustVersions {
559    Allow,
560    Fallback,
561}
562
563#[derive(Debug, Deserialize, PartialEq, Eq)]
564#[serde(rename_all = "kebab-case")]
565pub enum IncompatiblePublishAge {
566    Allow,
567    Deny,
568}
569
570#[derive(Copy, Clone, Debug, Deserialize)]
571#[serde(rename_all = "kebab-case")]
572pub enum FeatureUnification {
573    Package,
574    Selected,
575    Workspace,
576}
577
578/// The `[term]` table.
579///
580/// Example configuration:
581///
582/// ```toml
583/// [term]
584/// verbose = false
585/// quiet = false
586/// color = "auto"
587/// progress.when = "auto"
588/// ```
589#[derive(Debug, Deserialize, Default)]
590#[serde(rename_all = "kebab-case")]
591pub struct TermConfig {
592    pub verbose: Option<bool>,
593    pub quiet: Option<bool>,
594    pub color: Option<String>,
595    pub hyperlinks: Option<bool>,
596    pub unicode: Option<bool>,
597    pub progress: Option<ProgressConfig>,
598}
599
600/// The `term.progress` configuration.
601///
602/// Example configuration:
603///
604/// ```toml
605/// [term]
606/// progress.when = "never" # or "auto"
607/// ```
608///
609/// ```toml
610/// # `when = "always"` requires a `width` field
611/// [term]
612/// progress = { when = "always", width = 80 }
613/// ```
614#[derive(Debug, Default)]
615pub struct ProgressConfig {
616    pub when: ProgressWhen,
617    pub width: Option<usize>,
618    /// Communicate progress status with a terminal
619    pub term_integration: Option<bool>,
620}
621
622#[derive(Debug, Default, Deserialize)]
623#[serde(rename_all = "kebab-case")]
624pub enum ProgressWhen {
625    #[default]
626    Auto,
627    Never,
628    Always,
629}
630
631// We need this custom deserialization for validadting the rule of
632// `when = "always"` requiring a `width` field.
633impl<'de> Deserialize<'de> for ProgressConfig {
634    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
635    where
636        D: serde::Deserializer<'de>,
637    {
638        #[derive(Deserialize)]
639        #[serde(rename_all = "kebab-case")]
640        struct ProgressConfigInner {
641            #[serde(default)]
642            when: ProgressWhen,
643            width: Option<usize>,
644            term_integration: Option<bool>,
645        }
646
647        let pc = ProgressConfigInner::deserialize(deserializer)?;
648        if let ProgressConfigInner {
649            when: ProgressWhen::Always,
650            width: None,
651            ..
652        } = pc
653        {
654            return Err(serde::de::Error::custom(
655                "\"always\" progress requires a `width` key",
656            ));
657        }
658        Ok(ProgressConfig {
659            when: pc.when,
660            width: pc.width,
661            term_integration: pc.term_integration,
662        })
663    }
664}
665
666#[derive(Debug)]
667enum EnvConfigValueInner {
668    Simple(String),
669    WithOptions {
670        value: ConfigRelativePath,
671        force: bool,
672        relative: bool,
673    },
674}
675
676impl<'de> Deserialize<'de> for EnvConfigValueInner {
677    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
678    where
679        D: serde::Deserializer<'de>,
680    {
681        #[derive(Deserialize)]
682        struct WithOptions {
683            value: ConfigRelativePath,
684            #[serde(default)]
685            force: bool,
686            #[serde(default)]
687            relative: bool,
688        }
689
690        UntaggedEnumVisitor::new()
691            .string(|simple| Ok(EnvConfigValueInner::Simple(simple.to_owned())))
692            .map(|map| {
693                let with_options: WithOptions = map.deserialize()?;
694                Ok(EnvConfigValueInner::WithOptions {
695                    value: with_options.value,
696                    force: with_options.force,
697                    relative: with_options.relative,
698                })
699            })
700            .deserialize(deserializer)
701    }
702}
703
704/// Configuration value for environment variables in `[env]` section.
705///
706/// Supports two formats: simple string and with options.
707///
708/// ```toml
709/// [env]
710/// FOO = "value"
711/// ```
712///
713/// ```toml
714/// [env]
715/// BAR = { value = "relative/path", relative = true }
716/// BAZ = { value = "override", force = true }
717/// ```
718#[derive(Debug, Deserialize)]
719#[serde(transparent)]
720pub struct EnvConfigValue {
721    inner: EnvConfigValueInner,
722}
723
724impl EnvConfigValue {
725    /// Whether this value should override existing environment variables.
726    pub fn is_force(&self) -> bool {
727        match self.inner {
728            EnvConfigValueInner::Simple(_) => false,
729            EnvConfigValueInner::WithOptions { force, .. } => force,
730        }
731    }
732
733    /// Resolves the environment variable value.
734    ///
735    /// If `relative = true`,
736    /// the value is interpreted as a [`ConfigRelativePath`]-like path.
737    pub fn resolve<'a>(&'a self, cwd: &Path) -> Cow<'a, OsStr> {
738        match self.inner {
739            EnvConfigValueInner::Simple(ref s) => Cow::Borrowed(OsStr::new(s.as_str())),
740            EnvConfigValueInner::WithOptions {
741                ref value,
742                relative,
743                ..
744            } => {
745                if relative {
746                    let p = value.value().definition.root(cwd).join(value.raw_value());
747                    Cow::Owned(p.into_os_string())
748                } else {
749                    Cow::Borrowed(OsStr::new(value.raw_value()))
750                }
751            }
752        }
753    }
754}
755
756pub type EnvConfig = HashMap<String, EnvConfigValue>;
757
758/// `[registries.NAME]` tables.
759///
760/// The values here should be kept in sync with `GlobalRegistryConfig`
761#[derive(Deserialize, Clone, Debug)]
762#[serde(rename_all = "kebab-case")]
763pub struct RegistryConfig {
764    pub index: Option<String>,
765    pub token: OptValue<Secret<String>>,
766    pub credential_provider: Option<PathAndArgs>,
767    pub secret_key: OptValue<Secret<String>>,
768    pub secret_key_subject: Option<String>,
769    /// Minimum publish age threshold for RFC 3923
770    pub min_publish_age: Option<String>,
771    #[serde(rename = "protocol")]
772    _protocol: Option<String>,
773}
774
775/// The `[registry]` table, which has more keys than the `[registries.NAME]` tables.
776///
777/// Note: nesting `RegistryConfig` inside this struct and using `serde(flatten)` *should* work
778/// but fails with "invalid type: sequence, expected a value" when attempting to deserialize.
779#[derive(Deserialize)]
780#[serde(rename_all = "kebab-case")]
781pub struct GlobalRegistryConfig {
782    pub index: Option<String>,
783    pub token: OptValue<Secret<String>>,
784    pub credential_provider: Option<PathAndArgs>,
785    pub secret_key: OptValue<Secret<String>>,
786    pub secret_key_subject: Option<String>,
787    /// Global default Minimum publish age threshold for RFC 3923
788    pub global_min_publish_age: Option<String>,
789    #[serde(rename = "default")]
790    _default: Option<String>,
791    #[serde(rename = "global-credential-providers")]
792    _global_credential_providers: Option<Vec<String>>,
793}
794
795impl GlobalRegistryConfig {
796    pub fn to_registry_config(self) -> RegistryConfig {
797        RegistryConfig {
798            index: self.index,
799            token: self.token,
800            credential_provider: self.credential_provider,
801            secret_key: self.secret_key,
802            secret_key_subject: self.secret_key_subject,
803            // `min-publish-age` is per-registry config only.
804            min_publish_age: None,
805            _protocol: None,
806        }
807    }
808}